Compose UI测试基础:搭建环境,JUnit5与Compose Test Rule

UI测试这事儿,说白了就是个“信心工程”。你想想看,要是改了个文字颜色、调了个边距,心里都没底,那还怎么敢合并代码?我在项目中遇到过不止一次,觉得自己改的没问题,结果一跑测试,crash了。从那以后我养成了一个习惯——先搭好测试环境,再写业务代码。

一、环境搭建:Gradle依赖得配对

Compose的测试库分三块:UI测试核心、JUnit集成、以及调试工具。我个人习惯把它们放在一个测试专用的Module里,当然放在app模块的androidTest也行。关键是要把依赖版本对齐,否则你会看到一堆莫名其妙的NoSuchMethodError。

核心依赖(Compose BOM 2024.02.00 为例)

// 顶层 build.gradle 引入 BOM
dependencies {
    implementation platform('androidx.compose:compose-bom:2024.02.00')
    androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
    debugImplementation 'androidx.compose.ui:ui-test-manifest'
    // 如果你用 JUnit5,再加下面这行
    androidTestImplementation 'androidx.compose.ui:ui-test-junit5'
}

注意看,ui-test-manifest是debug依赖。我以前就犯过傻,把它加在release依赖里,结果签名包直接炸了。嗯,这里要强调一下:这个manifest只是为了在调试时能弹出测试Activity,正式版千万不能带。

二、JUnit5 与 Compose Test Rule

JUnit5 和 Compose 的配合其实很简单,核心就是一个 Rule:ComposeTestRule。但问题来了——JUnit4 有 @RunWith(AndroidJUnit4::class),JUnit5 用什么呢?答案是 @ExtendWith(AndroidJUnit5::class)。

下面是一个完整的测试类骨架,我一般会直接复制粘贴到新项目里:

import androidx.compose.ui.test.junit5.createComposeRule
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.Rule

@ExtendWith(AndroidJUnit5::class)
class MyComposeTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun testButtonShowsCorrectText() {
        composeTestRule.setContent {
            MyButton(text = "点我")
        }
        composeTestRule
            .onNodeWithText("点我")
            .assertExists()
    }
}

注意 @get:Rule 这个写法,JUnit5 的 Rule 必须放 @get:Rule 上,不然 Compose 不会帮你启动 UI。我曾经有一次就漏了 @get:,结果测试跑完了一堆空指针,查了半天才发现。

三、ComposeTestRule 的两个关键方法

你写测试时,最常用的是这两个:

方法作用个人小技巧
setContent 把 Composable 渲染到测试 Activity 我习惯在 @BeforeEach 里调用一次,避免每个测试都重复写
onNodeWithText / onNodeWithTag 查找 UI 节点 一定要给关键组件加 Modifier.testTag(),不然容易搜索到多个节点

四、避坑指南:我曾经踩过的三个大坑

  • 坑一:没有加 ui-test-manifest
    跑测试直接报 ActivityNotFoundException。记得只在 debugImplementation 里加。
  • 坑二:JUnit5 用了 @RunWith
    JUnit5 不用 @RunWith,要用 @ExtendWith。我当年刚迁移时,照搬 JUnit4 的写法,测试压根不执行。
  • 坑三:setContent 写在 @Test 里而不是 @BeforeEach
    导致每个测试都重复渲染,速度慢得让人崩溃。后来我写成 @BeforeEach,清爽多了。

五、知识图谱:Compose UI 测试环境概览

下面这张图把上面讲的内容串起来,你可以打印出来贴在工位上:

提示:垂直阅读,底层是依赖,中间是规则配置,顶层是测试执行。

Compose UI测试环境搭建知识体系 Compose测试环境 Gradle依赖配置 JUnit5 + Rule 测试用例编写 ui-test-junit4/5 debug manifest @ExtendWith createComposeRule setContent onNode断言 💡 最佳实践:将 setContent 放在 @BeforeEach 中 每个测试只关注业务断言,避免重复渲染

六、实际项目中的配置清单

我把平时项目里用的配置整理成了一个清单,方便你检查:

Tip: 我一般把这份清单放在项目 README 里,新同事来了直接抄作业。

  1. BOM 版本统一 —— 所有 Compose 相关库都用同一个 BOM
  2. 模块级 build.gradle 加 androidTestImplementation 和 debugImplementation
  3. 测试类注解 —— @ExtendWith(AndroidJUnit5::class)
  4. Rule 声明 —— @get:Rule + createComposeRule()
  5. @BeforeEach 里调用 setContent 初始化 UI
  6. 给关键组件加 testTag —— 比如 Modifier.testTag("submit_btn")

你想想看,只要按照这六步来,基本不会出大问题。我刚开始写 Compose 测试时,连 @get:Rule 都写错过,现在模板化之后,效率提高了不止一倍。

七、跑一下试试

环境配好之后,最简单的验证就是写一个空测试:

@Test
fun quickSmokeTest() {
    composeTestRule.setContent {
        Text("Hello World")
    }
    composeTestRule.onNodeWithText("Hello World").assertExists()
}

如果绿色通过,恭喜你,环境 ok 了。接下来就可以尽情写 Mock 回放测试了。嗯,这一步看似简单,却是后面所有高级玩法的基础——地基不牢,地动山摇。