测试无限kotlin协程



我有一个ViewModel。当它在屏幕上可见时,它就启动了。当用户离开屏幕时,它停止。当ViewModel启动时,我想每5秒执行一些代码。代码看起来像这样:

fun onStart() {
interval = launch(injectedDispatcher) {
while (true) {
doSomething()
delay(5000.milliseconds)
}
}
}
fun onStop() {
interval.cancel()
}

我想写一个集成测试,将测试这个ViewModel与它的依赖关系。我使用TestScope使这个集成测试变得即时:

val scope = TestScope()
val injectedDispatcher = StandardTestDispatcher(scope.testScheduler)
@Test
fun interval() = scope.runTest {
val viewModel = get(injectedDispatcher)
viewModel.onStart()
delay(30000) // <- execution will get stuck at this point
assertSomething(...)
viewModel.onStop()
}

如果在被测试的代码中没有无限循环,这个测试代码运行得很好。然而,如果至少有一个无限协程,delay(30000)将永远不会退出。相反,执行将卡在while (true)循环中,即使已经经过了30000ms。我还验证了scope.currentTime可以增加到30000ms以上,而while循环仍然不会退出。

我推测这是因为StandardTestDispatcher一直在while循环中循环,因为它一旦启动就不能挂起作业。

我做了一个小例子来说明这个问题:https://github.com/Alexey-/InfiniteTest

是否有一种方法挂起无限循环后运行它与TestDispatcher一个特定的时间?

问题似乎是TestScope.runTest将等待所有子协程在交付测试结果之前完成。
执行不会卡在delay(30_000)中。导致测试永远运行的原因是断言失败并抛出AssertionError。因为抛出了一个错误,下一行viewModel.onStop()永远不会被调用。这意味着在ViewModel中启动的协程永远不会完成,因此TestScope.runTest永远不会交付结果。
你可以很容易地测试:

...
println("after delay; before assertion")
try{
assertEquals(6, viewModel.count)
}catch (e: AssertionError){
e.printStackTrace()
throw e
}
println("after assertion")
viewModel.onStop()

最简单的解决方案是首先调用viewModel.onStop(),然后运行您想要的断言。


如果你想要一个完全替代的方法,你可以避免手动启动和停止你的视图模型,并选择一个更"协程"的方法。方法:


class AndroidTestViewModel(
val injectedDispatcher: CoroutineDispatcher = Dispatchers.IO
) : ViewModel() {
var count = 0
suspend fun doWhileInForeground(){
withContext(injectedDispatcher){
while (true) {
delay(5000)
count++
}
}
}
}

测试它可能看起来更像这样:

@Test
fun interval() = scope.runTest {
val viewModel = AndroidTestViewModel(injectedDispatcher)
launch {
viewModel.doWhileInForeground()
}
delay(30_000)
assertEquals(6,viewModel.count)
}

和一个例子使用在一个片段,这可以很容易地适应一个活动或喷气背包组成:

class SampleFragment: Fragment(){
val viewModel: AndroidTestViewModel = TODO()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch { 
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED){
viewModel.doWhileInForeground()
}
}
}
}

什么是testdispatcher和TestScope?A写下了我的观点。所有的工作。
测试看起来像。

val scope = TestScope()
val injectedDispatcher = StandardTestDispatcher(scope.testScheduler)
val model = TestViewModel()
@Test
fun interval() = scope.runTest {
val viewModel = model
viewModel.injectedDispatcher = injectedDispatcher
viewModel.onStart()
delay(30000) // <- execution will get stuck at this point   
viewModel.onStop()
Assert.assertTrue(model.count > 0)
}

模型看起来像

var injectedDispatcher = Dispatchers.IO
var interval: Job? = null
var count = 0
fun onStart() {
interval = viewModelScope.launch(injectedDispatcher) {
while (true) {
delay(5000)
count++
}
}
}
fun onStop() {
interval?.cancel()
}

所以当所有作业都关闭时,决定断言结果。当我们有assert错误时,我们有break from test。作用域正在工作,测试无法完成。

@Test
fun interval() = scope.runTest {
val viewModel = get(injectedDispatcher)
viewModel.onStart()
delay(30000) // <- execution will get stuck at this point
val result = getFromSomeWere() // <- what we wanna check
viewModel.onStop()
assertSomething(...) // <- check it here
}

@Test
fun androidInterval() = scope.runTest {
val viewModel = AndroidTestViewModel(injectedDispatcher)
try {
viewModel.onStart()
delay(30000)
assertEquals(6, viewModel.count)
} catch (e: AssertionError) {
viewModel.onStop()
throw e
}
viewModel.onStop()
}

最新更新