使用 Kotlin Coroutines 进行测试随机失败

Test with Kotlin Coroutines is randomly failing

让我们假设我们有一个 class 成员,其目的是 从两个不同的地方带来 2 个对象(假设 object1object2),然后创建最终对象 结果将这两个对象合并到另一个对象中,最终返回。

假设检索object1object2的操作可以并发, 所以这导致了 kotlin 协程的典型用例。

到目前为止所描述的内容如下例所示:

fun parallelCall(): MergedObject {
    return runBlocking(context = Dispatchers.Default) {
        try {
            val object1 : Deferred<Object1> = async {
                bringObject1FromSomewhere()
            }
            val object2 : Deferred<Object2> = async {
                bringObject2FromSomewhere()
            }

            creteFinalObject(object1.await(), object2.await())

        } catch (ex: Exception) {
            throw ex
        }
    }
}

周围的try块应该拦截任何抛出的异常 检索 object1object2,以及在 createFinalObject 方法中。

后者只是将之前调用的等待结果合并在一起, 等待两者的完成。 请注意,延迟对象 1 和对象 2 的等待几乎同时发生, 因为当它们作为参数传递给 createFinalObject 方法时都在等待。

在这种情况下,我可以使用 mockk 作为模拟库执行测试,这样每当 bringObject1FromSomewhere() 抛出异常,然后 creteFinalObject 方法被 NEVER 调用。即,类似于:

@Test
fun `GIVEN bringObject1FromSomewhere throws exception WHEN parallelCall executes THEN creteFinalObject is never executed`() {
      every { bringObject1FromSomewhere() } throws NullPointerException()
      every { bringObject2FromSomewhere() } returns sampleObject2

      assertThrows<NullPointerException> { parallelCall() }

      verify(atMost = 1) { bringObject1FromSomewhere() }
      verify(atMost = 1) { bringObject2FromSomewhere() }
      //should never be called since bringObject1FromSomewhere() throws nullPointer exception
      verify(exactly = 0) { creteFinalObject(any(), any()) }
  }

问题是上面的测试几乎总是有效,但是,在某些情况下它随机失败, 无论模拟值如何,都调用 createFinalObject 方法。

这个问题是否与延迟对象 1 和对象 2 的延迟时间略有不同有关? 在调用 creteFinalObject(object1.await(), object2.await()) 时等待?

我想到的另一件事可能是我在测试的最后一行中期望参数的方式: verify(exactly = 0) { creteFinalObject(any(), any()) } mockk 在使用 any() 时会有什么问题吗?

此外,try { } 块无法检测到异常可能是一个问题 在调用 createFinalObject 方法之前?在非并行环境中我永远不会怀疑这一点,但可能 runBlocking 作为 coroutineScope 的用法改变了游戏规则? 任何提示都会有所帮助,谢谢!

Kotlin 版本:1.6.0 Corutines 版本: 1.5.2 mockk 版本: 1.12.2

您确定它失败是因为它试图调用 creteFinalObject 函数吗?因为在阅读您的代码时,我认为那应该是不可能的(当然,永远不要说永远 :D)。只有object1.await()object2.await()return都成功才能调用creteFinalObject函数。

我认为还有其他事情正在发生。因为您正在执行 2 个独立的异步任务(获取对象 1 和获取对象 2),所以我怀疑这 2 个任务的顺序会导致成功或失败。

运行 你的代码在本地,我注意到它有时会在这一行失败:

verify(atMost = 1) { bringObject2FromSomewhere() }

而且我认为这是你的错误。如果在bringObject2FromSomewhere()之前调用bringObject1FromSomewhere(),则抛出异常,第二次函数调用永远不会发生,导致测试失败。反过来(2 在 1 之前)将使测试成功。 Dispatchers.Default 使用内部工作队列,其中在开始之前取消的作业将永远不会开始。而且第一个任务失败的速度足够快,以至于第二个任务根本无法启动。

我认为解决方法是改用 verify(atLeast = 0, atMost = 1) { bringObject2FromSomewhere() },但正如我在 MockK GitHub 问题页面上看到的那样,这还不受支持:https://github.com/mockk/mockk/issues/806

所以即使你指定 bringObject2FromSomewhere() 应该在 most 调用 1 次,它仍然会尝试验证它是否也在 least 调用 1次,不是这样。

您可以通过向异步调用添加延迟来获取第一个对象来验证这一点:

val object1 : Deferred<Object1> = async {
    delay(100)
    bringObject1FromSomewhere()
}

这样,测试总是成功的,因为bringObject2FromSomewhere()总是有足够的时间被调用。

那么如何解决这个问题呢?要么希望 MockK 修复指定 verify(atLeast = 0, atMost = 1) { ... } 的功能,要么暂时禁用此调用的验证。