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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 22:44:54