默认情况下,Compose 测试会与界面同步。通过
ComposeTestRule 调用
断言或操作时,测试将预先同步,直到界面树处于空闲状态。
通常,您无需执行任何操作。但是,您应该了解一些极端情况。
同步测试时,您可以使用虚拟时钟将 Compose 应用的时间提前。这意味着 Compose 测试不会实时运行,从而能够尽快通过测试。
但是,如果您不使用同步测试的方法,则不会发生任何重组,并且界面会暂停。
@Test
fun counterTest() {
val myCounter = mutableStateOf(0) // State that can cause recompositions.
var lastSeenValue = 0 // Used to track recompositions.
composeTestRule.setContent {
Text(myCounter.value.toString())
lastSeenValue = myCounter.value
}
myCounter.value = 1 // The state changes, but there is no recomposition.
// Fails because nothing triggered a recomposition.
assertTrue(lastSeenValue == 1)
// Passes because the assertion triggers recomposition.
composeTestRule.onNodeWithText("1").assertExists()
}请注意,此要求仅适用于 Compose 层次结构,而不适用于应用的其余部分。
停用自动同步功能
通过 ComposeTestRule(如 assertExists())调用断言或操作时,您的测试会与 Compose
界面同步。在某些情况下,您可能需要停止此同步并自行控制时钟。例如,您可以控制时间,以便在界面仍处于繁忙状态时对动画进行精确截图。如需停用自动同步功能,请将 mainClock 中的 autoAdvance 属性设置为 false:
composeTestRule.mainClock.autoAdvance = false
一般情况下,您需要自行将时间提前。您可以使用 advanceTimeByFrame() 仅提前一帧,或使用
advanceTimeBy() 提前一段特定时间:
composeTestRule.mainClock.advanceTimeByFrame()
composeTestRule.mainClock.advanceTimeBy(milliseconds)
空闲资源
Compose 可以同步测试和界面,以便以空闲状态完成各项操作和断言,从而根据需要等待或将时钟提前。但是,某些影响界面状态的异步操作可在后台运行,而测试无法得知这些结果。
在测试中创建并注册这些空闲资源,以便在确定受测应用是忙碌还是空闲时将这些资源考虑在内。 除非需要注册其他空闲资源(例如,如果您运行的后台作业未与 Espresso 或 Compose 同步),否则无需执行任何操作。
此 API 与 Espresso 的 空闲资源 非常相似,用于指示
受测对象是空闲还是忙碌。您可以使用 Compose 测试规则注册
IdlingResource的实现。
composeTestRule.registerIdlingResource(idlingResource)
composeTestRule.unregisterIdlingResource(idlingResource)
手动同步
在某些情况下,您必须将 Compose 界面与测试的其他部分或您测试的应用同步。
waitForIdle() 函数等待 Compose 空闲,但这
取决于 autoAdvance 属性:
composeTestRule.mainClock.autoAdvance = true // Default
composeTestRule.waitForIdle() // Advances the clock until Compose is idle.
composeTestRule.mainClock.autoAdvance = false
composeTestRule.waitForIdle() // Only waits for idling resources to become idle.
请注意,在这两种情况下,waitForIdle() 还会等待待处理的 绘制和布局
传递。
此外,您还可以将时钟提前,直到满足
advanceTimeUntil()的特定条件为止。
composeTestRule.mainClock.advanceTimeUntil(timeoutMs) { condition }
请注意,给定条件应当检查受到此时钟影响的状态(仅适用于 Compose 状态)。
优化动画测试
When testing high-fidelity animations, you often need to disable auto-advance
and manually step through frames to assert intermediate UI states. For these
specific frame-by-frame loops, use the runWithoutImplicitWait method to
execute your assertions. Standard node queries (like onNodeWithTag or
fetchSemanticsNode) trigger implicit synchronizations that are redundant
when you are manually controlling the clock, so bypassing them significantly
speeds up your test runtimes.
Usage guidelines
- Manual clock management: Use this API when
mainClock.autoAdvanceis set tofalseand the UI is in a known, stable state for the current frame. - UI thread execution: To ensure the stability of the UI tree, call
runWithoutImplicitWaiton the UI thread, such as withrunOnUiThread. Running it off the UI thread exposes your test to race conditions and stale state reads. - Read-only assertions: The block should strictly contain read-only assertions. Any actions that mutate state should be performed outside of this block.
Example
@Test fun runWithoutImplicitWaitSample() = runComposeUiTest { setContent { MainScreen() } mainClock.autoAdvance = false // Trigger an animation onNodeWithText("Start Animation").performClick() // Step through the animation frame-by-frame while (hasPendingWork()) { mainClock.advanceTimeByFrame() waitForIdle() runOnUiThread { // Suppress implicit synchronization inside this block to avoid redundant // waits on each node query, making the frame assertions execute much faster. runWithoutImplicitWait { val box1 = onNodeWithTag("Box1").fetchSemanticsNode() val box2 = onNodeWithTag("Box2").fetchSemanticsNode() val box3 = onNodeWithTag("Box3").fetchSemanticsNode() // Assert the exact intermediate state of all three properties for this frame assert(box1.boundsInRoot.right <= box2.boundsInRoot.left) assert(box2.boundsInRoot.right <= box3.boundsInRoot.left) } } } }
主线程同步
Compose 测试现在支持主线程同步,让您可以直接从主线程安全地调用 waitForIdle,并以此方式调用 Compose 界面操作和断言。
以前,Compose 测试严格执行双线程模型:测试作业发生在后台测试线程上,而界面更新发生在主线程上。从主线程(例如,在
runOnUiThread 块内)调用 waitForIdle 或 runOnIdle 等同步方法会抛出
IllegalStateException,因为框架强制执行严格的线程检查以防止主线程同步。
启用主线程同步后,即使在主线程上进行阻塞调用,Compose 测试框架现在也可以提前时钟并处理待处理的工作。
何时使用主线程同步
虽然将测试保留在后台线程上仍然是纯 Compose 测试的标准做法,但在以下几种特定情况下,主线程同步非常有利:
- 复杂的 View 互操作性:测试同时包含 Compose 和旧版 Android View 的混合界面时,操纵 View 通常需要在 主线程上运行。您现在可以按顺序与 View 交互并对 Compose 节点进行断言,而无需不断切换线程上下文。
- 同步状态突变:如果您的架构依赖于严格 绑定到主线程的状态持有者,您现在可以改变状态并立即等待 Compose 界面稳定,而无需离开主线程。
- 自定义测试运行程序:如果您要构建自定义测试基础架构或 利用测试运行程序本身在主 线程上执行的环境,Compose 测试现在可以干净地执行,而无需后台线程 委托。
示例
从历史上看,由于主线程上严格禁止同步,开发者必须在后台测试运行程序线程和界面线程之间来回切换,导致测试不连贯:
@Test fun testBidirectionalInteropUIUpdates_old() { val scenario = launchFragmentInContainer<InteropFragment>() composeTestRule.waitForIdle() scenario.onFragment { fragment -> fragment.legacyButton.performClick() } // Jump to Test Thread to verify state settles inside compose composeTestRule.waitForIdle() composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed() composeTestRule.onNodeWithText("Increment Legacy TextView").performClick() composeTestRule.waitForIdle() // Jump back to Main Thread to verify target view state settles scenario.onFragment { fragment -> assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1") } }
启用主线程同步后,Compose 和 View 层次结构的断言可以在同一块中执行:
@Test fun testBidirectionalInteropUIUpdates_new() { val scenario = launchFragmentInContainer<InteropFragment>() composeTestRule.waitForIdle() scenario.onFragment { fragment -> fragment.legacyButton.performClick() composeTestRule.waitForIdle() composeTestRule.onNodeWithText("Legacy Clicks: 1").assertIsDisplayed() composeTestRule.onNodeWithText("Increment Legacy TextView").performClick() composeTestRule.waitForIdle() assert(fragment.legacyTextView.text.toString() == "Compose Clicks: 1") } }
等待条件
依赖于外部工作(例如数据加载或 Android 的
测量或绘制,即 Compose 外部的测量或绘制)的任何条件应使用
更为宽泛的概念,例如 waitUntil():
composeTestRule.waitUntil(timeoutMs) { condition }
您还可以使用任何
waitUntil帮助程序:
composeTestRule.waitUntilAtLeastOneExists(matcher, timeoutMs)
composeTestRule.waitUntilDoesNotExist(matcher, timeoutMs)
composeTestRule.waitUntilExactlyOneExists(matcher, timeoutMs)
composeTestRule.waitUntilNodeCount(matcher, count, timeoutMs)
其他资源
- 在 Android 平台上测试应用:Android 测试主 着陆页提供了测试基础知识和技术的更广阔视野。
- 测试基础知识:详细了解 Android 应用测试背后的核心概念。
- 本地测试:您可以在自己的工作站上 本地运行一些测试。
- 插桩测试:最好也运行插桩测试。也就是说,直接在设备上运行的测试。
- 持续集成: 借助持续集成,您可以将测试集成到部署 流水线中。
- 测试不同的屏幕尺寸:由于用户可以使用多种设备,因此您应该针对不同的屏幕 尺寸进行测试。
- Espresso:虽然 Espresso 适用于基于 View 的 界面,但其知识对于 Compose 测试的某些方面仍然很有帮助。