使用MockK模拟异常调用resumeWithException时suspendCoroutine挂起
问题
测试外部API时无法自行构造其异常类型,使用MockK模拟IllegalStateException传入回调后,测试会无限挂起,无法从suspendCoroutine恢复;但传入真实IllegalStateException时测试正常抛出异常。示例代码如下:
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine class Foo { lateinit var cb: Callback interface Callback { fun good() fun bad(e: Throwable) } fun something(cb: Callback) { this.cb = cb } } class Test { @OptIn(DelicateCoroutinesApi::class) @Test fun test() { val foo = Foo() runBlocking { launch { delay(300) // 注释的MockK写法会导致挂起 // foo.cb.bad(mockk<IllegalStateException>(relaxed = true)) foo.cb.bad(IllegalStateException()) } GlobalScope.async { suspendCoroutine { cont -> foo.something(object : Foo.Callback { override fun good() { cont.resume(Unit) } override fun bad(e: Throwable) { cont.resumeWithException(e) } }) } }.await() } } }
原因
MockK默认生成的模拟Throwable对象会覆盖fillInStackTrace()等核心方法,而协程在处理resumeWithException时依赖这些方法的正常执行。模拟对象的异常行为会导致协程内部出错,无法完成挂起恢复,最终引发无限挂起。
解决方法
- 使用
spyk包装真实异常:保留Throwable的原有行为,同时可以按需修改特定方法:foo.cb.bad(spyk(IllegalStateException("Mocked error"))) - 显式模拟
Throwable的核心方法:如果必须直接Mock异常,手动指定fillInStackTrace()的返回值,确保协程能正常处理:val mockException = mockk<IllegalStateException> { every { fillInStackTrace() } returns this every { message } returns "Custom mocked message" } foo.cb.bad(mockException) - 替换
GlobalScope为局部作用域:GlobalScope的生命周期不受runBlocking管控,可能引发作用域不一致问题,改用runBlocking的子作用域:// 替换原GlobalScope.async代码 async { suspendCoroutine { cont -> foo.something(object : Foo.Callback { override fun good() { cont.resume(Unit) } override fun bad(e: Throwable) { cont.resumeWithException(e) } }) } }.await()
内容的提问来源于stack exchange,提问作者Thomas Keller
相关产品推荐
相关产品推荐

