Rob*_*ert 4 unit-testing kotlin kotlin-coroutines
我正在测试一个阻塞的协程。这是我的生产代码:
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)
}
}
}
Run Code Online (Sandbox Code Playgroud)
我的测试:
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()
}
}
Run Code Online (Sandbox Code Playgroud)
我将如何测试我的inc()函数?(界面是一成不变的,所以我无法将inc()变成挂起函数。)
这里有两个问题:
viewModel.inc()。让我们首先从问题 #2 开始:为此,您需要能够修改MyViewModel(但不能inc)并更改类,以便Dispatchers.IO它接收 aCoroutineContext作为参数,而不是使用硬编码。这样,您可以传入TestDispatcher测试,这将使用虚拟时间来快进延迟。您可以在Android 文档的注入 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)
}
}
}
Run Code Online (Sandbox Code Playgroud)
在这里,我还做了一些小的清理:
MyViewModel包含CoroutineScope而不是实现接口,这是官方推荐的做法coroutineContext传递给 的参数launch,因为在这种情况下它不执行任何操作 - 无论如何,相同的上下文都在范围内,因此它已经被使用对于问题#1,等待工作完成,您有几个选择:
如果您传入了TestDispatcher,则可以inc使用 等测试方法手动推进在内部创建的协程advanceUntilIdle。这并不理想,因为您非常依赖实现细节,而这是您在生产中无法做到的。但如果您不能使用下面更好的解决方案,它会起作用。
viewModel.inc()
advanceUntilIdle() // Returns when all pending coroutines are done
Run Code Online (Sandbox Code Playgroud)
正确的解决方案是让inc调用者知道它何时完成工作。您可以将其设为挂起方法,而不是在内部启动新的协程,但您声明无法修改该方法以使其挂起。另一种选择 - 如果您能够进行此更改 - 是inc使用async构建器创建新的协程,返回Deferred创建的对象,然后await()在调用站点进行 -ing。
override fun inc(): Deferred<Unit> {
scope.async {
delay(100)
_number.tryEmit(1)
}
}
// In the test...
viewModel.inc().await()
Run Code Online (Sandbox Code Playgroud)
如果您无法修改方法或类,则无法避免调用delay()导致真正的 100 毫秒延迟。在这种情况下,您可以强制测试等待该时间然后再继续。由于它使用 a 作为它创建的协程,因此常规delay()内部runTest将被快进TestDispatcher,但您可以使用以下解决方案之一:
// delay() on a different dispatcher
viewModel.inc()
withContext(Dispatchers.Default) { delay(100) }
// Use blocking sleep
viewModel.inc()
Thread.sleep(100)
Run Code Online (Sandbox Code Playgroud)
有关测试代码的一些最后说明:
Dispatchers.setMain,你就不需要传入testScheduler你TestDispatchers创建的。Main如果他们找到那里,他们会自动获取调度程序,如其文档中TestDispatcher所述。launchIn您可以简单地传入指向 的this接收者runTest,而不是创建一个新的作用域来传递给TestScope。| 归档时间: |
|
| 查看次数: |
4101 次 |
| 最近记录: |