Jetpack Compose测试单独运行通过但批量执行时触发无限组合循环错误
Jetpack Compose测试单独运行通过但批量执行时触发无限组合循环错误
我之前在项目里也碰到过几乎一模一样的问题——单测跑起来全绿,批量执行或者生成Jacoco报告就报AppNotIdleException,提示Compose一直无法进入idle状态。结合你的代码和现象,咱们来拆解下可能的原因和对应的解决办法:
可能的原因分析
- 测试隔离不彻底:Robolectric批量执行时,前一个测试的Compose状态、ViewModel Store或者Looper任务可能没被完全清理,干扰了当前测试的组合流程。
- ViewModel初始化的隐式依赖:你的
DocumentCheckLoadingScreen里给documentCheckLoadingViewModel设了默认参数viewModel(),虽然测试时传了mock,但批量运行时可能因为测试间的ViewModel Store残留,导致实际创建了真实的ViewModel而非使用mock? InfiniteCircularIndicator的潜在问题:这个自定义加载指示器的内部实现可能有坑——比如在Robolectric环境下,动画逻辑(比如animate*AsState、LaunchedEffect里的循环任务)没有被正确处理,导致无限触发重组。- Compose Idle检测的超时逻辑:批量执行时,测试框架的idle等待机制可能因为前序测试的残留异步任务,导致当前测试的断言还没完成就触发了超时。
针对性解决方案
试试下面这些办法,大概率能解决问题:
- 强制测试间的状态清理:在测试类里加
@Before和@After方法,确保每个测试前后Compose状态完全隔离:
@Before fun setup() { composeTestRule.setContent { /* 空内容,重置前一个测试的Compose树 */ } composeTestRule.waitForIdle() } @After fun teardown() { composeTestRule.setContent {} composeTestRule.waitForIdle() }
避免Composable的ViewModel默认参数依赖:把
DocumentCheckLoadingScreen的documentCheckLoadingViewModel参数改成非可选,或者用依赖注入框架(比如Hilt)来管理ViewModel,确保测试时能完全替换成mock,避免默认的viewModel()在批量环境下的意外行为。定位
InfiniteCircularIndicator的问题:暂时把InfiniteCircularIndicator替换成一个静态的Composable(比如一个简单的Box),再跑批量测试。如果不报错了,那就是这个指示器的实现有问题——比如检查它内部有没有在measure/layoutlambda里修改状态,或者动画在Robolectric下的适配问题。可以给指示器加个isInTest参数,测试时禁用动画逻辑。显式等待Compose Idle:在断言前主动调用
composeTestRule.waitForIdle(),确保Compose树稳定后再执行断言:
composeTestRule.setContent { // ... 你的Composable代码 } // 显式等待idle composeTestRule.waitForIdle() // 然后执行断言 composeTestRule.onNodeWithText(title).assertIsDisplayed() // ... 其他断言
- 调整Robolectric的Looper模式:在测试类上添加
@LooperMode(LooperMode.Mode.PAUSED)注解,Robolectric的PAUSED模式会更严格地处理线程和任务,避免批量执行时的Looper任务堆积。
你的代码参考
测试类代码
@RunWith(RobolectricTestRunner::class) internal class DocumentCheckLoadingScreenTest { @get:Rule val composeTestRule = createComposeRule() @Test fun testDocumentCheckLoadingScreenElementsVisibility() { val title = "Loading Title" val loadingMessage = "Loading your document..." composeTestRule.setContent { DocumentCheckLoadingScreen( title = title, loadingMessage = loadingMessage, documentCheckLoadingViewModel = mockk(relaxed = true) ) } composeTestRule.onNodeWithText(title).assertIsDisplayed() composeTestRule.onNodeWithText(loadingMessage).assertIsDisplayed() composeTestRule.onNodeWithText("Please Wait").assertIsDisplayed() composeTestRule.onNodeWithContentDescription("Loading Overlay").assertIsDisplayed() } }
Composable代码
@Composable internal fun DocumentCheckLoadingScreen( title: String = stringResource(R.string.cc_doc_check_title), loadingMessage: String = "", documentCheckLoadingViewModel: DocumentCheckLoadingViewModel = viewModel() ) { Column(modifier = Modifier .background(MaterialTheme.localSdkColors.backgroundColor) .fillMaxSize()) { Column(horizontalAlignment = Alignment.CenterHorizontally) { AlertDialogTitleText( modifier = Modifier.padding(top = 25.dp), text = title ) Column( modifier = Modifier .fillMaxSize() .background(MaterialTheme.localSdkColors.backgroundColor), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { InfiniteCircularIndicator() Spacer(modifier = Modifier.height(16.dp)) SubTitleText( modifier = Modifier.padding(top = 8.dp), text = loadingMessage, textAlign = TextAlign.Center, color = MaterialTheme.localSdkColors.headerTextColor ) SubTitleText( modifier = Modifier.padding(top = 8.dp).testTag("please_wait_text"), text = stringResource(R.string.cc_workflow_please_wait), textAlign = TextAlign.Center, color = MaterialTheme.localSdkColors.headerTextColor ) } } } }
内容来源于stack exchange
相关产品推荐
相关产品推荐

