第三章 第一个 Compose UI 测试用例:创建、编排与验证

在本章中,我们将动手编写第一个 Compose UI 测试用例。你不仅会学会如何搭建测试类,还会深入理解 createComposeRule 的内部原理、测试生命周期,以及点击验证的底层机制。通过本章的学习,你将建立起一套可复用的测试编写流程。

3.1 测试用例的核心原理

Compose 测试建立在 Compose 测试规则 (Test Rule) 之上。该规则负责管理 Compose 的合成 (Composition) 生命周期,并提供一个测试上下文,让测试代码可以直接与 UI 控件交互。

当你调用 createComposeRule() 时,系统会:

  • 创建一个独立的测试宿主 Activity(类似一个空壳 Activity);
  • 在该 Activity 上初始化 Compose 环境;
  • 挂载待测试的 Composable 函数;
  • 等待布局帧渲染完成,确保 UI 树可交互。

整个流程形成了一个闭环:创建环境 → 挂载内容 → 查找元素 → 执行动作 → 断言状态。下面这张流程图清晰地展示了这个核心流程。

Compose UI 测试用例执行流程 创建测试类 声明 @get:Rule createComposeRule() setContent 挂载 查找 UI 节点 执行点击 断言验证结果 关键原则 • 每一条测试用例应遵循“Arrange(准备) → Act(动作) → Assert(断言)”模式。 • Compose 测试框架自动处理空闲状态检查(Idling Resource),无需手动添加等待。

3.2 实现流程:编写你的第一个测试

3.2.1 创建测试类

测试类必须位于 src/androidTest 目录下,包名通常与待测试组件的包名一致或接近(例如 com.example.app)。

✏️ 最佳实践:建议每个 Composable 组件对应一个测试类,类名以 “Test” 结尾,例如 LoginButtonTest、ProfileCardTest。这样可以保持测试与源文件间的可追溯性。

3.2.2 声明 Compose 测试规则

使用 @get:Rule 注解声明一个 createComposeRule() 实例。这是框架与测试之间的桥梁。

import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule
import org.junit.Test

class ButtonClickTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun testButtonClick_changesText() {
        // 测试逻辑将在此处展开
    }
}

3.2.3 挂载 Composable 内容

在测试方法内,通过 composeTestRule.setContent { ... } 将待测试的 UI 代码挂载到测试宿主中。这一步对应流程图中的“挂载 UI”。

@Test
fun testButtonClick_changesText() {
    composeTestRule.setContent {
        // 直接编写或引用待测试的 Composable
        MyClickableButton()
    }
}

假设我们有一个简单的按钮组件 MyClickableButton:

@Composable
fun MyClickableButton() {
    var text by remember { mutableStateOf("点击我") }
    Button(onClick = { text = "已点击" }) {
        Text(text = text)
    }
}

3.2.4 查找元素并执行点击

Compose 测试使用语义树(Semantics Tree)来查找 UI 节点。常用的查找函数包括:

  • onNodeWithText("点击我") —— 通过显示文本查找;
  • onNodeWithTag("my_button") —— 通过 Modifier.testTag() 查找;
  • onNodeWithContentDescription("描述") —— 通过无障碍描述查找。

查找到节点后,调用 .performClick() 模拟用户点击:

composeTestRule.onNodeWithText("点击我")
    .performClick()

3.2.5 编写点击验证

点击后,使用 .assertIsDisplayed()、.assertTextEquals() 或 .assert(hasText("已点击")) 验证 UI 状态是否符合预期:

composeTestRule.onNodeWithText("已点击")
    .assertIsDisplayed()

// 或者使用更精确的文本匹配
composeTestRule.onNodeWithText("已点击")
    .assert(hasText("已点击"))

3.2.6 完整的测试用例

import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import org.junit.Rule
import org.junit.Test

class ButtonClickTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun testButtonClick_changesText() {
        // Arrange: 挂载测试组件
        composeTestRule.setContent {
            var text by remember { mutableStateOf("点击我") }
            Button(onClick = { text = "已点击" }) {
                Text(text = text)
            }
        }

        // Act: 查找按钮并执行点击
        composeTestRule.onNodeWithText("点击我")
            .performClick()

        // Assert: 验证文本变更
        composeTestRule.onNodeWithText("已点击")
            .assertIsDisplayed()
    }
}

3.3 运行测试

测试可以在 Android Studio 内直接运行,也可以通过 Gradle 命令行执行:

# 运行单个测试类
./gradlew :app:connectedDebugAndroidTest --tests "com.example.app.ButtonClickTest"

# 运行所有 Android 测试
./gradlew :app:connectedDebugAndroidTest

运行时,设备或模拟器上会自动打开测试宿主 Activity,对应的 UI 会一闪而过(通常不可见),但测试报告会立即在 Run 窗口中输出。

3.4 调试与常见错误

错误现象根本原因解决方案
@get:Rule 报错 “Cannot resolve symbol” 缺少 compose-ui-test 依赖 检查 module 级 build.gradle.kts 是否包含
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
onNodeWithText 找不到节点,抛出
AssertionError
文本与语义树中的字符串不匹配,或组件未渲染 使用 composeTestRule.onAllNodesWithText().fetchSemanticsNodes() 打印所有节点
performClick 后断言失败 异步状态更新尚未完成;或 Compose 重组未触发 Compose 测试框架应自动等待;若未生效,尝试 composeTestRule.waitForIdle()
测试类未被识别(No tests found) 测试方法缺少 @Test 注解,或测试类未继承 JUnit4 运行器 确保使用 @RunWith(AndroidJUnit4::class) 或 Gradle 配置 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

3.5 性能与稳定性建议

  • 避免在 setContent 中执行耗时操作:挂载应该是轻量级的,只包含 UI 定义;数据准备应该在 setContent 之外完成。
  • 善用 TestTag:为可交互组件添加稳定的 Modifier.testTag(),避免因文案变更导致测试失效。
  • 控制测试粒度:每个测试只验证一个行为。不要在一次测试中连续点击多个按钮,否则错误定位会变得困难。
🎯 本节点睛:
你已经亲手搭建了完整的 Compose UI 测试管道——从测试规则、UI 挂载到节点交互和断言。这个模式将贯穿本课程后续所有章节,请确保你完全理解了 createComposeRule 与 setContent 的作用。接下来,我们将深入探讨更丰富的节点匹配策略。