如何测试投掷的Kotlin Coroutine?

发布于 2025-02-03 06:29:52 字数 1269 浏览 7 评论 0 原文

kotlin 1.5.21,kotlinx-coroutines-test 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 ,在测试中,它被A testCoroutinesCope 代替。 dispatchers.io dispatchers.io 的代理,在测试中,它是 testCoroutInedInedisPatcher 。在这种情况下,如果 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是从Coroutine中抛出异常的正确方法,如果是,我该如何测试?

(Kotlin 1.5.21, kotlinx-coroutines-test 1.5.0)

Please consider the following code inside a 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. The dispatchers.IO is a proxy to Dispatchers.IO, in tests it is a 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. Now I'm trying to (JUnit4) test this code:

@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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

东北女汉子 2025-02-10 06:29:53
  1. 为什么测试是绿色的:

代码启动{...} 与与
测试方法。要识别它尝试修改 maythrow 方法(请参阅代码
下面的摘要),因此它返回结果忽略了正在发生的事情
在内部启动{...} 上,将测试红色替换启动
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
}
  1. 为什么看起来像“测试失败,因为同样的例外……”

测试不会失败,但是我们看到了控制台中的异常stacktrace,因为
默认异常处理程序可以使用并应用它,因为在这种情况下,自定义异常处理程序 coroutineExceptionHandler throws(


  1. 如何测试

功能 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
}
  1. Why the test is green:

code in launch{ ... } is beeing executed asynchronously with the
test method. 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)


@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)

  1. How to test

Function mayThrow has too many responsibilities, that is why it is hard to test. 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

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
}
难以启齿的温柔 2025-02-10 06:29:53

如果没有其他作用,我可以建议将代码(将异常)移动到另一种方法并测试此方法:

// 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 允许使用 /a>方法。例子:

assertThrows<IllegalStateException> { tested.doWorkThatThrows() }

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. Example:

assertThrows<IllegalStateException> { tested.doWorkThatThrows() }
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文