首页 > 解决方案 > 如何在单元测试中等待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但是由于我的代码是在CoroutineScope提供的viewModelScopethis 上启动的,所以它不起作用。如果可能的话,我不希望在任何地方都注入 CoroutineScope,因为任何较低级别的类都可以(并且确实)像示例中那样制作自己的 CoroutineScope,然后测试必须了解很多有关实现细节的信息。作用域背后的想法是每个类都可以控制取消它们的异步操作。例如 viewModelScope 在 vi​​ewModel 被销毁时默认被取消。

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

标签: unit-testingkotlinkotlin-coroutines

解决方案


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

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)
}

从 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()
    }
}

这是测试模型和测试

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)
    }
}

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


推荐阅读