Compose 测试 API 的 v2 版本(createComposeRule、createAndroidComposeRule、runComposeUiTest、runAndroidComposeUiTest 等)现已上架,可更好地控制协程执行。此更新不会复制整个 API surface,仅更新了用于建立测试环境的 API。
v1 API 已弃用,强烈建议您迁移到新 API。迁移可验证您的测试是否符合标准协程行为,并避免未来出现兼容性问题。如需查看已弃用的 v1 API 的列表,请参阅 API 映射。
v1 API 依赖于 UnconfinedTestDispatcher,而 v2 API 默认使用 StandardTestDispatcher 来运行组合。此变更使 Compose 测试行为与标准 runTest API 保持一致,并提供了对协程执行顺序的显式控制。
配置测试环境
Compose 测试 v2 API 使用 ComposeUiTestConfig 自定义测试环境。为测试创建设置函数的 API(例如 createComposeRule、runComposeUiTest 和其他相关 API)接受 ComposeUiTestConfig。此配置对象将 effectContext、runTestContext 和 testTimeout 等环境相关 API 整合到一个对象中。
配置模型还管理 inputMode。Compose 测试 v2 API 默认会在每次测试开始时强制执行 InputMode.Touch,以确保确定性并防止输入模式状态在测试之间泄露。
ComposeUiTestConfig 是 Compose 测试 v2 API 的一部分,默认使用 StandardTestDispatcher。如果您的测试使用 v1 API,请先参阅迁移到 v2 测试 API,然后再采用 ComposeUiTestConfig。
迁移到 ComposeUiTestConfig
在用于在测试中创建设置函数的重载中,接受单个配置参数(例如 effectContext、runTestContext 或 testTimeout)的多个重载已被弃用。请更新您的测试,改为使用 ComposeUiTestConfig,如以下示例所示:
val testConfig = ComposeUiTestConfig( effectContext = EmptyCoroutineContext, runTestContext = EmptyCoroutineContext, testTimeout = 30.seconds ) @get:Rule val rule = createComposeRule(config = testConfig) // OR runComposeUiTest(config = testConfig) {}
默认输入模式
如果测试依赖于在测试开始前通过插桩 API 配置的非触控输入模式,则可能会在迁移期间失败。在测试的设置函数中,系统默认会在每次测试开始时强制执行 InputMode.Touch,以提高确定性并防止状态泄露,从而覆盖环境设备状态和测试前设置。
如需解决此问题,请在 ComposeUiTestConfig 中指定所需的输入模式:
class FocusTest { @get:Rule val rule = createComposeRule( config = ComposeUiTestConfig(inputMode = InputMode.Keyboard) ) @Test fun testFocus() {} }
如需为单个测试用例(而非整个测试类)配置输入模式,请将 ComposeUiTestConfig 传递给 runComposeUiTest:
class FocusTest { @Test fun testTouchMode() = runComposeUiTest { // Runs with the default InputMode.Touch } @Test fun testKeyboardMode() = runComposeUiTest( ComposeUiTestConfig(inputMode = InputMode.Keyboard) ) { // Runs with InputMode.Keyboard } }
如需了解其他迁移问题和解决方法,请参阅常见无法过审情况及解决方法。
迁移到 v2 测试 API
升级到 v2 API 时,您通常可以使用查找 + 替换来更新软件包导入并采用新的调度程序更改。
或者,您也可以使用以下提示,问问 Gemini 执行迁移到 Compose 测试 API v2 的操作:
AI 提示
从 v1 测试 API 迁移到 v2 测试 API
此提示将使用本指南迁移到 v2 测试 API。
Migrate to Compose testing v2 APIs using the official
migration guide.请使用下表将已弃用的 v1 API 映射到其 v2 替代项:
已弃用 (v1) |
换货 (v2) |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
向后兼容性和例外情况
现有的 v1 API 现已弃用,但会继续使用 UnconfinedTestDispatcher,以维持现有行为并防止破坏性更改。
以下是默认行为发生变化的唯一例外情况:
用于在 AndroidComposeUiTestEnvironment 类中运行组合的默认测试调度程序已从 UnconfinedTestDispatcher 切换到 StandardTestDispatcher。这会影响您使用构造函数或子类 AndroidComposeUiTestEnvironment 创建实例并调用该构造函数的情况。
主要变化:对协程执行的影响
API 的 v1 和 v2 之间的主要区别在于协程的分派方式:
- v1 API (
UnconfinedTestDispatcher):协程启动后,会立即在当前线程上执行,通常在下一行测试代码运行之前完成。与生产环境中的行为不同,这种立即执行可能会无意中掩盖实际应用中可能发生的真实时间问题或竞态条件。 - v2 API (
StandardTestDispatcher):启动协程后,协程会加入队列,并且在测试明确推进虚拟时钟之前不会执行。标准 Compose 测试 API(例如waitForIdle())已经可以处理此同步,因此大多数依赖这些标准 API 的测试应该可以继续运行,而无需进行任何更改。
常见无法过审情况及解决方法
如果您的测试在升级到 v2 后失败,则可能存在以下模式:
- 失败:您启动了一项任务(例如,ViewModel 加载数据),但断言立即失败,因为数据仍处于“正在加载”状态。
- 原因:使用 v2 API 时,协程会排队,而不是立即执行。任务已加入队列,但在检查结果之前从未实际运行。
- 解决方法:明确推进时间。您必须明确告知 v2 调度程序何时执行工作。
旧方法
在 v1 中,任务会立即启动并完成。在 v2 中,以下代码会失败,因为 loadData() 实际上尚未运行。
// In v1, this launched and finished immediately.
viewModel.loadData()
// In v2, this fails because loadData() hasn't actually run yet!
assertEquals(Success, viewModel.state.value)
建议的方法
在断言之前,使用 waitForIdle 或 runOnIdle 执行已加入队列的任务。
方法 1:使用 waitForIdle 会推进时钟,直到界面处于空闲状态,从而验证协程已运行。
viewModel.loadData()
// Explicitly run all queued tasks
composeTestRule.waitForIdle()
assertEquals(Success, viewModel.state.value)
方法 2:使用 runOnIdle 会在界面空闲后在界面线程上执行代码块。
viewModel.loadData()
// Run the assertion after the UI is idle
composeTestRule.runOnIdle {
assertEquals(Success, viewModel.state.value)
}
手动同步
在涉及手动同步的场景中(例如停用自动推进时),启动协程不会导致立即执行,因为测试时钟已暂停。如需在不推进虚拟时钟的情况下执行队列中的协程,请使用 runCurrent() API。这会运行为当前虚拟时间安排的任务。
composeTestRule.mainClock.scheduler.runCurrent()
与 waitForIdle() 会推进测试时钟直到界面稳定不同,runCurrent() 会在保持当前虚拟时间的同时执行待处理任务。此行为可实现对中间状态的验证,否则,如果时钟提前到空闲状态,这些中间状态将被跳过。
测试环境中使用的底层测试调度程序会公开。此调度程序可与 Kotlin runTest API 结合使用,以同步测试时钟。
迁移到 runComposeUiTest
如果您将 Compose 测试 API 与 Kotlin runTest API 搭配使用,强烈建议您改用 runComposeUiTest。
旧方法
将 createComposeRule 与 runTest 结合使用会创建两个单独的时钟:一个用于 Compose,另一个用于测试协程作用域。此配置可能会强制您手动同步测试调度器。
@get:Rule val composeTestRule = createComposeRule() @Test fun testWithCoroutines() { composeTestRule.setContent { var status by remember { mutableStateOf("Loading...") } LaunchedEffect(Unit) { delay(1000) status = "Done!" } Text(text = status) } // NOT RECOMMENDED // Fails: runTest creates a new, separate scheduler. // Advancing time here does NOT advance the compose clock. // To fix this without migrating, you would need to share the scheduler // by passing 'composeTestRule.mainClock.scheduler' to runTest. runTest { composeTestRule.onNodeWithText("Loading...").assertIsDisplayed() advanceTimeBy(1000) composeTestRule.onNodeWithText("Done!").assertIsDisplayed() } }
建议的方法
runComposeUiTest API 会自动在其自己的 runTest 范围内执行测试块。测试时钟会与 Compose 环境同步,因此您不再需要手动管理调度程序。
@Test fun testWithCoroutines() = runComposeUiTest { setContent { var status by remember { mutableStateOf("Loading...") } LaunchedEffect(Unit) { delay(1000) status = "Done!" } Text(text = status) } onNodeWithText("Loading...").assertIsDisplayed() mainClock.advanceTimeBy(1000 + 16 /* Frame buffer */) onNodeWithText("Done!").assertIsDisplayed() } }