我有课和考试。我想在单个线程上执行所有协程。目前测试失败:
class MainClass {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
var value = 0
fun run() {
scope.launch(Dispatchers.IO) {
delay(1000)
withContext(Dispatchers.Main) {
withContext(Dispatchers.IO) {
delay(1000)
withContext(Dispatchers.Main) {
value = 1
}
}
}
}
}
}
class TestTest {
@Test
fun test() = runTest {
val test = MainClass()
test.run()
assertTrue(test.value == 1)
}
}
如何才能通过测试?
问题是您在
run
中启动了一个协程,但在测试中您尝试在协程完成之前访问其结果 (value = 1
)。
这是测试异步代码时的常见问题。解决方案是使用使用“虚拟”时间的 TestDispatcher,这样您就可以让协程以受控的方式执行,例如逐步进行或快进。 这意味着您需要在测试中使用与在 MainClass 中硬编码的调度程序不同的调度程序。这是您应该
注入调度员的主要原因:
class MainClass(
val dispatcherIO: CoroutineDispatcher = Dispatchers.IO,
) {
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
var value = 0
fun run() {
scope.launch(dispatcherIO) {
delay(1000)
withContext(Dispatchers.Main) {
withContext(dispatcherIO) {
delay(1000)
withContext(Dispatchers.Main) {
value = 1
}
}
}
}
}
}
此代码的行为与您问题中的代码相同,但测试现在可以提供不同的 IO 调度程序。
Dispatchers.Main
仍然是硬编码的。其原因是主调度程序在测试中的处理方式略有不同。有一种特殊的方法可以在运行时替换主调度程序,因此只需将 IO 调度程序设为变量即可。
测试现在看起来像这样:class TestTest {
@Test
fun test() = runTest {
val testDispatcher = StandardTestDispatcher(testScheduler)
Dispatchers.setMain(testDispatcher)
try {
val test = MainClass(testDispatcher)
test.run()
advanceUntilIdle()
assertTrue(test.value == 1)
} finally {
Dispatchers.resetMain()
}
}
}
testDispatcher
是现在使用的调度程序。首先它被传递到
Dispatchers.setMain()
以取代主调度程序。然后它也被传递给 MainClass
的构造函数,因此它也被用作 IO 调度程序。您可以在这里使用不同的 TestDispatcher,但我认为没有理由为您当前的代码执行此操作。实际的测试代码现在由一个 try 块包装,之后需要该块来重置主调度程序。不过,通常您会使用 JUnit 测试规则来设置和重置主调度程序,以使测试代码与设置逻辑保持清晰。请参阅此处了解更多信息:https://developer.android.com/kotlin/coroutines/test#setting-main-dispatcher到目前为止,更改仅适用于您的测试设置。您需要对实际测试逻辑进行的唯一更改是行
advanceUntilIdle()
:使用
test.run()
启动协程后,现在将暂停当前代码,直到 testScheduler
上的所有协程完成。因此,当您在下一行中测试 test.value == 1
时,这实际上是正确的,并且您的测试成功通过。