使用 Kotlin Coroutines 进行测试随机失败
Test with Kotlin Coroutines is randomly failing
让我们假设我们有一个 class 成员,其目的是
从两个不同的地方带来 2 个对象(假设 object1 和 object2),然后创建最终对象
结果将这两个对象合并到另一个对象中,最终返回。
假设检索object1和object2的操作可以并发,
所以这导致了 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块应该拦截任何抛出的异常
检索 object1 和 object2,以及在 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) { ... }
的功能,要么暂时禁用此调用的验证。
让我们假设我们有一个 class 成员,其目的是 从两个不同的地方带来 2 个对象(假设 object1 和 object2),然后创建最终对象 结果将这两个对象合并到另一个对象中,最终返回。
假设检索object1和object2的操作可以并发, 所以这导致了 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块应该拦截任何抛出的异常
检索 object1 和 object2,以及在 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) { ... }
的功能,要么暂时禁用此调用的验证。