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

单元测试基于CallbackFlow的广播接收器时触发Turbine超时无值返回错误

单元测试基于CallbackFlow的广播接收器时触发Turbine超时无值返回错误

我来帮你排查这个Turbine超时的问题,咱们一步步拆解问题、解决问题:

先揪出代码里的几个致命小错误

这些细节问题直接导致了你的测试无法正常触发接收器逻辑:

  • 测试类里的ontentManager变量名少打了一个c,应该是contentManager(虽然mock的时候写对了,但变量名笔误容易出问题)
  • 初始化接收器时,lateinit var consentUpdatedBroadcastReceiverImp: consentUpdatedBroadcastReceiverImp类名首字母小写了,要和实际类名ConsentUpdatedBroadcastReceiverImp保持一致
  • 最核心的问题:你实际代码里用ContextCompat.registerReceiver注册广播,但测试里mock的是context.registerReceiver,这俩完全不是同一个方法!所以测试根本没捕获到实际注册的接收器,调用onReceive自然不会有值流入Turbine
  • 测试里发送广播用的Action是OTBroadcastServiceKeys.OT_CONSENT_UPDATED,但实际代码监听的是BroadcastServiceKeys.CONSENT_UPDATED,Action不匹配的话,接收器根本不会响应

修正后的测试代码&关键解释

我把所有问题都修正了,同时优化了协程调度的逻辑,确保测试能正常触发流的发射:

class ConsentUpdatedBroadcastReceiverImpTest {
    private val contentManager = mock<ContentManager>()
    private val testDispatcher = StandardTestDispatcher()
    private val applicationScope = TestScope(testDispatcher)
    private val context = mock<Context>()
    private lateinit var consentUpdatedBroadcastReceiverImp: ConsentUpdatedBroadcastReceiverImp

    @Before
    fun setUp() {
        consentUpdatedBroadcastReceiverImp = ConsentUpdatedBroadcastReceiverImp(
            context = context,
            contentManager = contentManager,
            applicationScope = applicationScope
        )
    }

    @Test
    fun `given after receiving a broadcast then send broadcast`() = runTest(testDispatcher) {
        // Given
        val consentStatusPreference = mock<ConsentStatusPreference> {
            on { performance } doReturn ConsentStatusType.DENIED
            on { functionality } doReturn ConsentStatusType.ACCEPTED
            on { targeting } doReturn ConsentStatusType.NOT_COLLECTED
        }
        whenever(contentManager.getConsentStatusPreference()).thenReturn(consentStatusPreference)

        // 关键:mock ContextCompat的静态注册方法,而不是Context的实例方法
        var registeredReceiver: BroadcastReceiver? = null
        whenever(
            ContextCompat.registerReceiver(
                eq(context),
                any<BroadcastReceiver>(),
                eq(IntentFilter(BroadcastServiceKeys.CONSENT_UPDATED)),
                eq(ContextCompat.RECEIVER_NOT_EXPORTED)
            )
        ).thenAnswer {
            registeredReceiver = it.getArgument(1)
            null // 模拟注册成功的返回值
        }

        // mock注销方法,避免测试抛出未mock的调用异常
        doNothing().`when`(context).unregisterReceiver(any<BroadcastReceiver>())

        // When
        consentUpdatedBroadcastReceiverImp.observe.test {
            // 发送和接收器监听一致的Action的广播
            registeredReceiver?.onReceive(context, Intent(BroadcastServiceKeys.CONSENT_UPDATED))
            // 推进测试调度器,确保所有协程任务执行完毕
            advanceUntilIdle()
            
            // Then
            val receivedPreference = awaitItem()
            assert(receivedPreference === consentStatusPreference)
            cancelAndIgnoreRemainingEvents()
        }
    }
}

关键修正点说明

  1. mock正确的注册方法:
    实际代码用的是静态方法ContextCompat.registerReceiver,所以必须mock这个方法才能捕获到注册的接收器(需要确保你的测试依赖里加了mockito-inline,不然没法mock静态方法)
  2. Action严格匹配:
    发送广播的Intent Action必须和接收器监听的BroadcastServiceKeys.CONSENT_UPDATED完全一致,不然接收器不会响应
  3. 协程调度同步:
    用runTest(testDispatcher)指定测试调度器,同时调用advanceUntilIdle()确保所有协程任务(包括shareIn的流订阅、CallbackFlow的发射)都执行完毕
  4. 处理注销逻辑:
    当CallbackFlow的awaitClose触发时会调用unregisterReceiver,提前mock这个方法避免测试抛出异常

额外小建议

  • 如果ConsentStatusPreference是数据类,建议直接创建实例而不是mock,这样测试更稳定,也减少mock带来的额外复杂度
  • 确保你的测试gradle依赖里已经添加了mockito-inline,不然没法mockContextCompat的静态方法

这样修改后,你的测试应该就能正常捕获到流里的值,不会再出现Turbine超时的问题了。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 09:34:31