社で何これって聞かれてした説明を見えるところに文章で残しておこうと思ったもの。
👀
- ドキュメント: https://developer.android.com/reference/androidx/arch/core/executor/testing/InstantTaskExecutorRule
- コード: https://cs.android.com/androidx/platform/frameworks/support/+/1b2b8e2a1dbb4f524af976c8b3c254d6dee974f9:arch/core/core-testing/src/main/java/androidx/arch/core/executor/testing/InstantTaskExecutorRule.java
使い方
class MyTest { @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() fun `test your component using AAC library`() { // LiveData とか Room とかが絡むテスト } }
予備知識: @Rule
, TestRule
, TestWatcher
について
@Rule
は対象が rule (もしくは rule を返却するもの) であることを示す annotationTestRule
はテストの振る舞いやレポートに対する規則を実装するための interfaceTestWatcher
はTestRule
の1つで、これを継承してテストの各フェイズで任意のアクションを実行させる rule を作ることができる- Wiki: Rules · junit-team/junit4 Wiki · GitHub
InstantTaskExecutorRule
の中身
小さいクラスなので実際のコードは上記の Code Search 等で見てもらうとして、やってることは以下のとおり:
- テスト実行前に
ArchTaskExecutor
の delegate を設定する- 設定した delegate は
isMainThread
は常にtrue
を返し、executeOnDiskIO
とpostToMainThread
は与えられたRunnable
をそのまま実行する
- 設定した delegate は
- テスト終了時に
ArchTaskExecutor
に設定した delegate を破棄する
ArchTaskExecutor
ArchTaskExecutor
は、非同期タスクの実行を担う singleton object で、 LiveData
や LifecycleRegistry
, RoomDatabase
で使われている。Main thread か IO thread でタスクを実行するためのAPI (postToMainThread
, executeOnDiskIO
) を持ち、またそれらでタスクを実行する Executor
を取り出すこともできる。
ArchTaskExecutor
にはもう一つ、今実行されている thread が main thread かを判定する isMainThread
という API がある。これは main thread 上での実行を要求している API 内部で assertion のために使われ、例えば LiveData
では observe するときや setValue
の中で呼ばれている。
タスクの実行部分は delegate されており、 InstantTaskExecutorRule
が行っているように setDelegate
を使って外から設定することができる。ユーザが delegate を与えない場合は DefaultTaskExecutor
が使用される。
DefaultTaskExecutor
の postToMainThread
では、main looper に対する Handler
を使ってタスクを main thread に post している。 isMainThread
は、 main looper に紐ついてる thread と current thread を比較している。
JUnit local test と main looper
そもそもなんで使う必要があるのかというと、Android アプリとしてコードを実行する時と local test を実行する時の環境の差が関係している。Android アプリとして実行される際は、大雑把に説明すると、アプリのプロセスが起動した後まず ActivityThread
で main looper の準備と実行が行われ*1、それから諸々の準備ができた後に我々の書いたアプリケーションコードが動く。しかし local test は JUnit test として実行されるので、Android アプリに関わるセットアップの類は実行されない。これがどう関係するかというと、local test 実行時には main looper のセットアップが行われず Looper#getMainLooper
は null
のままとなるので、それに依存する Handler
や、 main looper をもとに "Android アプリにおける main thread" を判定する処理が機能しなくなってしまう。
class MyTest { fun test() { val mainLooper = Looper.getMainLooper() // null val livedata = MutableLiveData<Int>() livedata.observeForever { println("onChanged") } // java.lang.NullPointerException // at androidx.arch.core.executor.DefaultTaskExecutor.isMainThread // at androidx.arch.core.executor.ArchTaskExecutor.isMainThread // at androidx.lifecycle.LiveData.assertMainThread // at androidx.lifecycle.LiveData.observeForever } }
この問題に対しては 1) main looper をテスト実行前に用意する、もしくはあるように偽装する、2) Looper
や Handler
に触れない形に変える、という解決方法を考えることができ、 2) を実現する手段として InstantTaskExecutorRule
が用意されている、ということになる。
Robolectric による解決と比較
ところで Robolectric を使っても main looper の問題を解決することは可能である。Robolectric はテスト実行時に Android framework のコードを実行したり APIを偽装してくれたりする。つまり上で挙げた 1) のアプローチを取ることになる。今回のケースでは、テストケース実行時には Looper
に main looper が準備された状態になっているため、 ArchTaskExecutor
で DefaultTaskExecutor
を使っても NPE でクラッシュすることはなくなる。
@RunWith(AndroidJUnit4::class) class MyTestWithRobolectric { fun `test with robolectric`() { val mainLooper = Looper.getMainLooper() // non-null value val livedata = MutableLiveData<Int>() livedata.observeForever { println("onChanged") } // observe できる } }
ただし Robolectric による Android framework 部分のセットアップには少なくとも数秒以上の時間がかかるので、「ユニットテスト」として評価した場合は有意に遅くなる *2。あと、非同期処理の待ち合わせについて何かやってくれるということはないので、例えば postValue
している箇所がある場合は特別な考慮が必要になる。
@RunWith(AndroidJUnit4::class) class MyTestWithRobolectric { fun `test with robolectric`() { val livedata = MutableLiveData<Int>() var count = 0 livedata.observeForever { println("onChanged: $it") count++ } livedata.value = 1 // テスト実行中に observer に通知が届く = 処理される livedata.postValue(2) // 通知が届かない assertThat(count).isEquelTo(2) // Expected:2, Actual:1 } }
InstantTaskExecutorRule
は ArchTaskExecutor
の delegate を差し替えるだけなので非同期タスク処理の関わる部分がここのみの場合のみ有効だが、その分テストの実行時間への影響は小さい。また ArchTaskExecutor
を通して行われる非同期処理もただ同期的に実行するようになるので、待ち合わせについて考慮する必要がなくなる。
class MyTestWithInstantTaskExecutorRule { @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() fun `test with InstantTaskExecutorRule`() { val livedata = MutableLiveData<Int>() var count = 0 livedata.observeForever { println("onChanged: $it") count++ } livedata.value = 1 // ArchTaskExecutor#postToMainThead の振る舞いが同期化されるので、 // postValue もテスト実行時に observer に通知が届くようになる livedata.postValue(2) assertThat(count).isEquelTo(2) // passed } }
使い分けとしては, LiveData や Room しか関わらないような local test については InstantTaskExecutorRule
だけ使って、他にも Android framework への依存があるようなコンポーネントに対する local test では Robolectric も使う、みたいな感じで良いと思う。
まとめ
- AAC のコンポーネントのいくつかはスレッド確認や非同期実行を行う際に
ArchTaskExecutor
を使用している ArchTaskExecutor
は振る舞いの実体を delegate しており、デフォルトではDefaultTaskExecutor
を使用しているDefaultTaskExecutor
は main thread の確認にLooper#getMainLooper
を参照している- Local test は Android framework に関わるセットアップを行ってくれないため、 main looper の準備も行われない
InstantTaskExecutorRule
はArchTaskExecutor
の delegate を差し替えて、実行 thread の確認を何も見ずに答えたり非同期実行を担う箇所を同期的に処理したりするように挙動を変更することで、local test でも AAC 関連の非同期処理や main thread 制約の関わる部分をよしなに動くようにしてくれる
*1:この辺は『Android を支える技術 <I>』の 1.3.2 あたりを読むと良い
*2:Robolectric が「高速」と謳われる文脈は実機や emulator を用いたデバイステストとの比較で、apk をビルドしてデバイスにインストールして実行するのと比べたら当然高速