迁移到 v2 测试 API

Compose 测试 API 的 v2 版本(createComposeRulecreateAndroidComposeRulerunComposeUiTestrunAndroidComposeUiTest 等)现已推出,可用于更好地控制协程执行。此更新不会复制整个 API surface,只会更新用于建立测试环境的 API。

v1 API 已弃用,强烈建议您迁移到新 API。迁移可验证您的测试是否符合标准协程行为,并避免日后出现兼容性问题。如需查看已弃用的 v1 API 的列表,请参阅 API 映射

这些更改已包含在 androidx.compose.ui:ui-test-junit4:1.11.0-alpha03+androidx.compose.ui:ui-test:1.11.0-alpha03+ 中。

v1 API 依赖于 UnconfinedTestDispatcher,而 v2 API 默认使用 StandardTestDispatcher 来运行组合。此变更使 Compose 测试行为与标准 runTest API 保持一致,并提供对协程执行顺序的明确控制。

API 映射

升级到 v2 API 时,您通常可以使用查找和替换来更新软件包导入并采用新的调度程序更改。

或者,您也可以使用以下提示让 Gemini 执行迁移到 Compose 测试 API v2 的操作:

从 v1 测试 API 迁移到 v2 测试 API

此提示将使用本指南迁移到 v2 测试 API。

Migrate to Compose testing v2 APIs using the official
migration guide.

使用 AI 提示

AI 提示旨在用于 Android Studio 中的 Gemini。

如需详细了解 Studio 中的 Gemini,请访问以下网址:https://developer.android.com/studio/gemini/overview

使用下表将已弃用的 v1 API 映射到其 v2 替代项:

已弃用 (v1)

替换 (v2)

androidx.compose.ui.test.junit4.createComposeRule

androidx.compose.ui.test.junit4.v2.createComposeRule

androidx.compose.ui.test.junit4.createAndroidComposeRule

androidx.compose.ui.test.junit4.v2.createAndroidComposeRule

androidx.compose.ui.test.junit4.createEmptyComposeRule

androidx.compose.ui.test.junit4.v2.createEmptyComposeRule

androidx.compose.ui.test.junit4.AndroidComposeTestRule

androidx.compose.ui.test.junit4.v2.AndroidComposeTestRule

androidx.compose.ui.test.runComposeUiTest

androidx.compose.ui.test.v2.runComposeUiTest

androidx.compose.ui.test.runAndroidComposeUiTest

androidx.compose.ui.test.v2.runAndroidComposeUiTest

androidx.compose.ui.test.runEmptyComposeUiTest

androidx.compose.ui.test.v2.runEmptyComposeUiTest

androidx.compose.ui.test.AndroidComposeUiTestEnvironment

androidx.compose.ui.test.v2.AndroidComposeUiTestEnvironment

向后兼容性和例外情况

现有的 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)

使用 waitForIdlerunOnIdle 在断言之前执行排队的任务。

方法 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

之前的做法

createComposeRulerunTest 结合使用会创建两个单独的时钟:一个用于 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()
    }
}