简体   繁体   English

如何对抛出的 kotlin 协程进行单元测试?

[英]How to unit test a kotlin coroutine that throws?

( Kotlin 1.5.21, kotlinx-coroutines-test 1.5.0 ) Kotlin 1.5.21,kotlinx-coroutines-test 1.5.0

Please consider the following code inside a androidx.lifecycle.ViewModel :请考虑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 corresponds to viewModelScope , in tests it is replaced by a TestCoroutineScope . vmScope对应viewModelScope ,在测试中它被TestCoroutineScope取代。 The dispatchers.IO is a proxy to Dispatchers.IO , in tests it is a TestCoroutineDispatcher . dispatchers.IODispatchers.IO的代理,在测试中它是一个TestCoroutineDispatcher In this case, the app's behavior is undefined if bar() returns null, so I want it to crash if that's the case.在这种情况下,如果bar()返回 null,应用程序的行为是未定义的,所以我希望它在这种情况下崩溃。 Now I'm trying to (JUnit4) test this code:现在我正在尝试(JUnit4)测试这段代码:

@Test(expected = IllegalStateException::class)
fun `should crash if something goes wrong with bar`()  {
    tested.mayThrow()
}

The test fails because of the very same exception it is supposed to test for:测试失败是因为它应该测试的异常相同:

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

I have the feeling I'm missing something quite obvious here... Question: is the code in my ViewModel the right way to throw an exception from a coroutine and if yes, how can I unit test it?我觉得我在这里遗漏了一些非常明显的东西......问题:我的 ViewModel 中的代码是否是从协程中抛出异常的正确方法,如果是,我如何对其进行单元测试?

If nothing else works I can suggest to move the code, which throws an exception, to another method and test this method:如果没有别的工作,我可以建议将抛出异常的代码移动到另一个方法并测试这个方法:

// 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()
}

Or using JUnit Jupiter allows to test throwing Exceptions by using assertThrows method.或者使用JUnit Jupiter允许使用assertThrows方法测试抛出异常。 Example:例子:

assertThrows<IllegalStateException> { tested.doWorkThatThrows() }
  1. Why the test is green:为什么测试是绿色的:

code in launch{... } is beeing executed asynchronously with the test method. launch{... }中的代码与测试方法异步执行。 To recognize it try to modify mayThrow method (see code snippet below), so it returns a result disregarding of what is going on inside launch {...} To make the test red replace launch with runBlocking (more details in docs , just read the first chapter and run the examples)要识别它,请尝试修改mayThrow方法(请参阅下面的代码片段),因此它会返回一个结果,而runBlocking launch launch {...}第一章并运行示例)


@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
}
  1. Why it looks like "test fails because of the very same exception..."为什么它看起来像“由于完全相同的异常而导致测试失败......”

the test does not fail, but we see the exception stacktrace in console, because the default exception handler works so and it is applied, because in this case the custom exception handler CoroutineExceptionHandler throws ( detailed explanation )测试没有失败,但我们在控制台中看到异常堆栈跟踪,因为默认异常处理程序如此工作并且它被应用,因为在这种情况下自定义异常处理程序CoroutineExceptionHandler抛出(详细解释

  1. How to test如何测试

Function mayThrow has too many responsibilities, that is why it is hard to test. Function mayThrow职责太多,难测。 It is a standard problem and there are standard treatments ( first , second ): long story short is apply Single responsibility principle.这是一个标准问题,并且有标准处理方法(第一第二):长话短说是应用单一责任原则。 For instance, pass exception handler to the function例如,将异常处理程序传递给 function

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
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM