Kotlin协程中带Mutex的测试失败原因及解决方案
为何使用Mutex时Kotlin协程测试会失败?如何解决?
问题重现
代码示例
class ClassA { suspend fun return10(): String { delay(1000) // 模拟耗时操作 return "10" } } object MyClass { private val mClassA by lazy { ClassA() } private val mMutex = Mutex() suspend fun performOperationWithMutex(): String { mMutex.withLock { return mClassA.return10() } } suspend fun performOperationWithoutMutex(): String = mClassA.return10() } @RunWith(RobolectricTestRunner::class) @Config(application = TestApplication::class) class MyTest { private val mMyClass = spyk<MyClass>() private val mClassA = mockk<ClassA>(relaxed = true, relaxUnitFun = true) @Before fun setUp() { every { mMyClass getProperty "mClassA" } returns mClassA } @ExperimentalCoroutinesApi @Test fun testWithoutMutexSucceeds() = runBlockingTest { mMyClass.performOperationWithoutMutex() coVerify (exactly = 1) { mClassA.return10() } } @ExperimentalCoroutinesApi @Test fun testWithMutexFails() = runBlockingTest { mMyClass.performOperationWithMutex() coVerify (exactly = 1) { mClassA.return10() } } }
失败报错
Verification failed: call 1 of 1: ClassA(#5).return10(any())) was not called
不带Mutex的测试执行成功,但带Mutex的测试无法验证ClassA.return10()的调用。
原因分析
核心问题是**Mutex.withLock的挂起逻辑与runBlockingTest使用的TestCoroutineDispatcher不兼容**:
runBlockingTest依赖TestCoroutineDispatcher自动跳过delay等挂起操作,同步执行协程,但该调度器对Mutex内部的锁等待挂起处理不完善。- 当调用
mMutex.withLock时,协程会进入挂起状态等待锁(即使锁是空闲的),而TestCoroutineDispatcher没有自动推进这个挂起任务,导致mClassA.return10()从未被执行,最终coVerify验证失败。 - 另外,
spyk对单例MyClass的代理逻辑,也可能加剧了协程调度与mock对象调用的路由问题。
解决方案
方案1:使用新版测试API(推荐)
从Kotlin协程1.6.0开始,runBlockingTest已被标记为过时,推荐使用StandardTestDispatcher配合runTest,它能正确处理Mutex这类挂起逻辑:
@OptIn(ExperimentalCoroutinesApi::class) @Test fun testWithMutexSucceeds() = runTest { mMyClass.performOperationWithMutex() coVerify(exactly = 1) { mClassA.return10() } }
方案2:手动推进旧版测试调度器
如果必须使用runBlockingTest,可以在调用方法后手动调用advanceUntilIdle(),强制调度所有挂起的协程任务:
@ExperimentalCoroutinesApi @Test fun testWithMutexFixed() = runBlockingTest { mMyClass.performOperationWithMutex() advanceUntilIdle() // 强制推进所有挂起的协程 coVerify(exactly = 1) { mClassA.return10() } }
方案3:调整业务代码结构(可选)
虽然不解决核心调度问题,但可以优化withLock块的返回逻辑,让代码更规范,避免潜在的协程跳转问题:
suspend fun performOperationWithMutex(): String { return mMutex.withLock { mClassA.return10() } }
内容的提问来源于stack exchange,提问作者Damia Fuentes
相关产品推荐
相关产品推荐

