我正在测试一个阻塞的协程。这是我的生产代码:
interface Incrementer {
fun inc()
}
class MyViewModel : Incrementer, CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO
private val _number = MutableStateFlow(0)
fun getNumber(): StateFlow<Int> = _number.asStateFlow()
override fun inc() {
launch(coroutineContext) {
delay(100)
_number.tryEmit(1)
}
}
}
还有我的测试:
class IncTest {
@BeforeEach
fun setup() {
Dispatchers.setMain(StandardTestDispatcher())
}
@AfterEach
fun teardown() {
Dispatchers.resetMain()
}
@Test
fun incrementOnce() = runTest {
val viewModel = MyViewModel()
val results = mutableListOf<Int>()
val resultJob = viewModel.getNumber()
.onEach(results::add)
.launchIn(CoroutineScope(UnconfinedTestDispatcher(testScheduler)))
launch(StandardTestDispatcher(testScheduler)) {
viewModel.inc()
}.join()
assertEquals(listOf(0, 1), results)
resultJob.cancel()
}
}
如何测试inc()函数?(接口是用石头刻的,所以我不能把inc()变成一个suspend函数。)
这里有两个问题:
- 您希望等待
viewModel.inc()
在内部启动的协同程序中完成的工作 - 理想情况下,100毫秒的延迟应该在测试过程中快进,这样实际上就不需要100毫秒来执行
让我们先从问题#2开始:为此,您需要能够修改MyViewModel
(但不能修改inc
),并更改类,以便它接收CoroutineContext
作为参数,而不是使用硬编码的Dispatchers.IO
。这样,您就可以在测试中传递TestDispatcher
,这将使用虚拟时间来加快延迟。您可以在Android文档的Injecting TestDispatchers部分中看到这种模式。
class MyViewModel(coroutineContext: CoroutineContext) : Incrementer {
private val scope = CoroutineScope(coroutineContext)
private val _number = MutableStateFlow(0)
fun getNumber(): StateFlow<Int> = _number.asStateFlow()
override fun inc() {
scope.launch {
delay(100)
_number.tryEmit(1)
}
}
}
在这里,我还做了一些小的清理:
- 使
MyViewModel
包含CoroutineScope
,而不是实现接口,这是官方建议的做法 - 删除了传递给
launch
的coroutineContext
参数,因为在这种情况下它什么都不做——无论如何,相同的上下文都在作用域中,所以它已经被使用了
对于问题#1,等待工作完成,您有几个选项:
如果您已经传入了
TestDispatcher
,则可以使用advanceUntilIdle
等测试方法手动推进在inc
内创建的协程。这并不理想,因为您在很大程度上依赖于实现细节,而这是您在生产中无法做到的。但如果你不能使用下面更好的解决方案,它会起作用。viewModel.inc() advanceUntilIdle() // Returns when all pending coroutines are done
正确的解决方案是
inc
让其调用者知道它何时完成了工作。您可以将其作为一个挂起方法,而不是在内部启动一个新的协同程序,但您声明不能修改该方法使其挂起。如果您能够进行此更改,另一种选择是使用async
构建器在inc
中创建新的协同程序,返回创建的Deferred
对象,然后在调用站点执行await()
。override fun inc(): Deferred<Unit> { scope.async { delay(100) _number.tryEmit(1) } } // In the test... viewModel.inc().await()
如果不能修改方法或类,则无法避免
delay()
调用导致实际100ms延迟。在这种情况下,您可以强制测试等待该时间后再继续。runTest
中的常规delay()
将被快速转发,这要归功于它为创建的协同程序使用TestDispatcher
,但您可以使用以下解决方案之一:// delay() on a different dispatcher viewModel.inc() withContext(Dispatchers.Default) { delay(100) } // Use blocking sleep viewModel.inc() Thread.sleep(100)
关于测试代码的一些最后注释:
- 由于您正在执行
Dispatchers.setMain
,因此不需要将testScheduler
传递到您创建的TestDispatchers
中。如果他们在Main
中找到一个TestDispatcher
,他们会自动从那里获取调度程序,如文档中所述 - 您可以简单地传入指向
TestScope
的runTest
的接收器this
,而不是创建一个新的作用域来传入launchIn