如何在单元测试中等待 suspendCoroutine?

fin*_*usl 6 unit-testing kotlin kotlin-coroutines

我想为我的 android 应用程序编写测试。有时 viewModel 会使用 Kotlins 协程启动函数在后台执行任务。这些任务在 androidx.lifecycle 库提供的 viewModelScope 中执行。为了仍然测试这些功能,我用 Dispatchers.Unconfined 替换了默认的 android Dispatchers,它同步运行代码。

至少在大多数情况下。使用 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()
        }
    }
}
}
Run Code Online (Sandbox Code Playgroud)

我正在尝试使用runBlocking,但是只有当您使用CoroutineScope创建的runBlocking. 但是由于我的代码是在此CoroutineScope提供的情况下启动的,viewModelScope因此将无法正常工作。如果可能的话,我不想在任何地方注入 CoroutineScope,因为任何较低级别的类都可以(并且确实)像示例中那样创建自己的 CoroutineScope,然后测试必须了解很多关于实现的细节。作用域背后的想法是每个类都可以控制取消它们的异步操作。例如, viewModelScope 在 vi​​ewModel 被销毁时默认取消。

我的问题: 我可以使用什么协程调度程序来运行启动和 withContext 等阻塞(如Dispatchers.Unconfined)并运行 suspendCoroutine 阻塞?

And*_*ana 6

将协程上下文传递给您的模型怎么样?就像是

class Model(parentContext: CoroutineContext = Dispatchers.Default) {
    private val modelScope = CoroutineScope(parentContext)
    var result = false

    fun doWork() {
        modelScope.launch {
            Thread.sleep(3000)
            result = true
        }
    }
}

@Test
fun test() {
    val model = runBlocking {
        val model = Model(coroutineContext)
        model.doWork()
        model
    }
    println(model.result)
}
Run Code Online (Sandbox Code Playgroud)

从 androidx.lifecycle更新viewModelScope 你可以只使用这个测试规则

@ExperimentalCoroutinesApi
class CoroutinesTestRule : TestWatcher() {
    private val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()

    override fun starting(description: Description?) {
        super.starting(description)
        Dispatchers.setMain(dispatcher)
    }

    override fun finished(description: Description?) {
        super.finished(description)
        Dispatchers.resetMain()
        dispatcher.cleanupTestCoroutines()
    }
}
Run Code Online (Sandbox Code Playgroud)

这是测试模型和测试

class MainViewModel : ViewModel() {
    var result = false

    fun doWork() {
        viewModelScope.launch {
            Thread.sleep(3000)
            result = true
        }
    }
}

class MainViewModelTest {
    @ExperimentalCoroutinesApi
    @get:Rule
    var coroutinesRule = CoroutinesTestRule()

    private val model = MainViewModel()

    @Test
    fun `check result`() {
        model.doWork()
        assertTrue(model.result)
    }
}
Run Code Online (Sandbox Code Playgroud)

不要忘记添加testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"以获得TestCoroutineDispatcher


fin*_*usl 6

在其他答案的帮助下,我找到了以下解决方案。正如 sedovav 在他的回答中所建议的,我可以异步运行任务并等待 Deferred。基本上是相同的想法,我可以通过启动运行任务并等待处理该任务的作业。这两种情况的问题是,我如何获得延期或工作。

我找到了解决这个问题的方法。这样的 Job 始终是 CoroutineContext 中包含的 Job 的子级,CoroutineContext 是 CoroutineScope 的一部分。所以下面的代码确实解决了这个问题,无论是在我的示例代码中还是在实际的 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
}
Run Code Online (Sandbox Code Playgroud)

它看起来确实有点像黑客攻击,所以如果有人知道为什么这是危险的,请告诉我。如果存在由其他类创建的另一个底层 CoroutineScope,它也将不起作用。但这是我拥有的最好的解决方案。