fresh digitable

めんどくさかったなってことを振り返ったり振り返らなかったりするための記録

テストの後片付けでLiveData.removeObserver()すると怒られることがある

はじめに

そんなことをやる必要はありません。あくまでもネタとしてお読みください。

今回の:

org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3
org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.3
androidx.lifecycle:lifecycle-livedata-ktx:2.2.0

顛末

ViewModelのテストで、LiveDataFlowの監視を開始したり終了したり流れてきた値をチェックする処理を簡単にできるようにしたいと考えて、そのようなクラスを作り現行の処理と置き換えたところ、次のようにコルーチンの処理が全部終わってないといって怒られた。

kotlinx.coroutines.test.UncompletedCoroutinesError: Unfinished coroutines during teardown. Ensure all coroutines are completed or cancelled by your test.
    at kotlinx.coroutines.test.TestCoroutineDispatcher.cleanupTestCoroutines(TestCoroutineDispatcher.kt:178)
        ...

試しにコルーチンを起動させている個所をコメントアウトするなどしてみたが状況は変わらず、特に変わったことしてないはずなのにおかしいなーなどと思いながらコードを見直したところ、良かれと思って後片付けの箇所に足した処理があったのを思い出した。

override fun finished(description: Description?) {  // TestWatcher の関数
    super.finished(description)
    observers.forEach { (l, o -> l.removeObserver(o) }   // これ
    scope.cancel()
}

該当の処理をコメントアウトしたところ、怒られが解消した。

解説

androidx.lifecycle.liveData()androidx.lifecycle.asLiveData()で作ったLiveData *1 は、activeになった時に内部のコルーチンスコープを使ってFlow.collect()を開始する。その後、inactiveな状態になると一定時間(デフォルトでは5秒)待ったあとでFlow.collect()Jobをキャンセルするようになっている。この一定時間待つという処理にdelay()が使われている。ということは当然コルーチンが起動しているので、これが完了するのを待たなければならない。テスト冒頭でDispatchers.setMain()をしつつ、kotlinx.coroutines.test.runBlockingTest()やそれに相当する関数を使うなどして、例えば、

    runBlockingTest {
        observers.forEach { (l, o -> l.removeObserver(o) }
    }

としてやることでdelay()を含んだJobを消化できる。

ちなみに

androidx.lifecycle.asFlow()の中ではJobのキャンセル時にGlobalScopeなコルーチンが起動するのでこれも終わるまで待てないとだめ(一敗)。

*1:asLiveData()は内部でliveData()を呼んでいる