You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 10:52:16