(Kotlin 1.5.21,kotlinx协同程序测试1.5.0)
请考虑androidx.lifecycle.ViewModel
:中的以下代码
fun mayThrow(){
val handler = CoroutineExceptionHandler { _, t -> throw t }
vmScope.launch(dispatchers.IO + handler) {
val foo = bar() ?: throw IllegalStateException("oops")
withContext(dispatchers.Main) {
_someLiveData.value = foo
}
}
}
vmScope
对应于viewModelScope
,在测试中它被TestCoroutineScope
代替。dispatchers.IO
是Dispatchers.IO
的代理,在测试中它是TestCoroutineDispatcher
。在这种情况下,如果bar()
返回null,则应用程序的行为是未定义的,所以如果是这种情况,我希望它崩溃。现在我正在尝试(JUnit4)测试这个代码:
@Test(expected = IllegalStateException::class)
fun `should crash if something goes wrong with bar`() {
tested.mayThrow()
}
测试失败是因为它应该测试的异常:
Exception in thread "Test worker @coroutine#1" java.lang.IllegalStateException: oops
// stack trace
Expected exception: java.lang.IllegalStateException
java.lang.AssertionError: Expected exception: java.lang.IllegalStateException
// stack trace
我感觉我错过了一些显而易见的东西。。。问题:ViewModel中的代码是从协同程序抛出异常的正确方法吗?如果是,我如何对其进行单元测试?
- 为什么测试是绿色的:
launch{ ... }
中的代码与测试方法。要识别它,请尝试修改mayThrow
方法(参见代码下面的代码段),因此它会返回一个结果,而不管发生了什么在launch {...}
内部将launch
替换为runBlocking
(更多文档中的详细信息,请阅读第一章并运行示例)
@Test
fun test() {
assertEquals(1, mayThrow()) // GREEN
}
fun mayThrow(): Int {
val handler = CoroutineExceptionHandler { _, t -> throw t }
vmScope.launch(dispatchers.IO + handler) {
val foo = bar() ?: throw IllegalStateException("oops")
withContext(dispatchers.Main) {
_someLiveData.value = foo
}
}
return 1 // this line succesfully reached
}
- 为什么它看起来像"由于相同的异常,测试失败">
测试没有失败,但我们在控制台中看到异常stacktrace,因为默认的异常处理程序是这样工作的,并且它是应用的,因为在这种情况下,自定义异常处理程序
CoroutineExceptionHandler
抛出(详细解释)
- 如何测试
函数mayThrow
的职责太多,这就是它很难测试的原因。这是一个标准问题,有标准的处理方法(第一,第二):长话短说就是应用单一责任原则。例如,将异常处理程序传递给函数
fun mayThrow(xHandler: CoroutineExceptionHandler){
vmScope.launch(dispatchers.IO + xHandler) {
val foo = bar() ?: throw IllegalStateException("oops")
withContext(dispatchers.Main) {
_someLiveData.value = foo
}
}
}
@Test(expected = IllegalStateException::class)
fun test() {
val xRef = AtomicReference<Throwable>()
mayThrow(CoroutineExceptionHandler { _, t -> xRef.set(t) })
val expectedTimeOfAsyncLaunchMillis = 1234L
Thread.sleep(expectedTimeOfAsyncLaunchMillis)
throw xRef.get() // or assert it any other way
}
如果其他方法都不起作用,我可以建议将引发异常的代码移动到另一个方法,并测试这个方法:
// ViewModel
fun mayThrow(){
vmScope.launch(dispatchers.IO) {
val foo = doWorkThatThrows()
withContext(dispatchers.Main) {
_someLiveData.value = foo
}
}
}
fun doWorkThatThrows(): Foo {
val foo = bar() ?: throw IllegalStateException("oops")
return foo
}
// Test
@Test(expected = IllegalStateException::class)
fun `should crash if something goes wrong with bar`() {
tested.doWorkThatThrows()
}
或者使用JUnit Jupiter
允许使用assertThrows
方法测试抛出异常。示例:
assertThrows<IllegalStateException> { tested.doWorkThatThrows() }