web-dev-qa-db-ja.com

ユニットテストでsuspendCoroutineを待つ方法は?

Androidアプリケーションのテストを作成したい。viewModelはKotlinsコルーチン起動関数を使用してバックグラウンドでタスクを実行することがあります。これらのタスクは、androidx.lifecycleライブラリが提供するviewModelScopeで実行されます。これらの関数を引き続きテストするために、デフォルトのAndroid DispatchersをDispatchers.Unconfinedに置き換えました。これは、コードを同期的に実行します。

少なくともほとんどの場合。 suspendCoroutineを使用する場合、Dispatchers.Unconfinedは一時停止されず、後で再開されますが、代わりに単に戻ります。 Dispatchers.Unconfinedのドキュメントは、その理由を明らかにしています。

[Dispatchers.Unconfined]を使用すると、対応するサスペンド関数で使用されているスレッドでコルーチンを再開できます。

したがって、私の理解では、コルーチンは実際には中断されていませんが、suspendCoroutineの後の残りの非同期関数はcontinuation.resumeを呼び出すスレッドで実行されます。したがって、テストは失敗します。

例:

class TestClassTest {
var testInstance = TestClass()

@Test
fun `Test with default dispatchers should fail`() {
    testInstance.runAsync()
    assertFalse(testInstance.valueToModify)
}

@Test
fun `Test with dispatchers replaced by Unconfined should pass`() {
    testInstance.DefaultDispatcher = Dispatchers.Unconfined
    testInstance.IODispatcher = Dispatchers.Unconfined
    testInstance.runAsync()
    assertTrue(testInstance.valueToModify)
}

@Test
fun `I need to also test some functions that use suspend coroutine - How can I do that?`() {
    testInstance.DefaultDispatcher = Dispatchers.Unconfined
    testInstance.IODispatcher = Dispatchers.Unconfined
    testInstance.runSuspendCoroutine()
    assertTrue(testInstance.valueToModify)//fails
}
}

class TestClass {
var DefaultDispatcher: CoroutineContext = Dispatchers.Default
var IODispatcher: CoroutineContext = Dispatchers.IO
val viewModelScope = CoroutineScope(DefaultDispatcher)
var valueToModify = false

fun runAsync() {
    viewModelScope.launch(DefaultDispatcher) {
        valueToModify = withContext(IODispatcher) {
            sleep(1000)
            true
        }
    }
}

fun runSuspendCoroutine() {
    viewModelScope.launch(DefaultDispatcher) {
        valueToModify = suspendCoroutine {
            Thread {
                sleep(1000)
                //long running operation calls listener from background thread
                it.resume(true)
            }.start()
        }
    }
}
}

私はrunBlockingを試していましたが、CoroutineScopeによって作成されたrunBlockingを使用してlaunchを呼び出す場合にのみ役立ちます。しかし、私のコードはCoroutineScopeによって提供されるviewModelScopeで起動しているため、これは機能しません。可能であれば、CoroutineScopeをどこにでも注入したくないのは、例のように下位レベルのクラスが独自のCoroutineScopeを作成できる(そして作成する)ためです。テストでは、実装の詳細について多くのことを知る必要があります。スコープの背後にある考え方は、すべてのクラスが非同期操作のキャンセルを制御できるということです。たとえば、viewModelScopeは、viewModelが破棄されると、デフォルトでキャンセルされます。

私の質問:launchやwithContextなどのブロッキング(Dispatchers.Unconfinedなど)を実行し、suspendCoroutineブロッキングも実行するコルーチンディスパッチャーを使用できますか?

4
findusl

他の答えの助けを借りて、私は次の解決策を見つけました。 sedovavが彼の答えで示唆したように、私はタスクを非同期で実行し、延期を待つことができます。基本的に同じ考えで、起動時にタスクを実行し、タスクを処理するジョブを待つことができます。どちらの場合も問題は、どのように延期またはジョブを取得するかです。

私はこれに対する解決策を見つけました。このようなジョブは常に、CoroutineScopeの一部であるCoroutineContextに含まれるジョブの子です。したがって、次のコードは、私のサンプルコードと実際のAndroidアプリケーションの両方で、問題を解決します。

@Test
fun `I need to also test some functions that use suspend coroutine - How can I do that?`() {
    replaceDispatchers()
    testInstance.runSuspendCoroutine()
    runBlocking {
        (testInstance.viewModelScope.coroutineContext[Job]?.children?.forEach { it.join() }
    }
    assertTrue(testInstance.valueToModify)//fails
}

ハックのように見えますが、誰かがこれが危険である理由がある場合は、教えてください。また、他のクラスによって作成された別の基になるCoroutineScopeがある場合は、機能しません。しかし、それは私が持っている最良の解決策です。

0
findusl

これはまさにrunBlockingが行うために作成されたものです。 GlobalScope.launch呼び出しを置き換えるだけです。

result = runBlocking {
    suspendCoroutine { continuation ->
        Thread {
            sleep(1000)
            //long running operation calls listener from background thread
            continuation.resume(true)
        }.start()
    }
}
0
Kiskae