Compose 测试概述:为什么它和传统 View 测试不一样?
说起 Android 测试,大家第一反应可能是 Espresso + ActivityScenario。但自从 Jetpack Compose 出来以后,测试的方式发生了不小的变化。嗯,我最早接触 Compose 测试是在 2021 年底,那时公司刚把新模块切到 Compose,我还在想:「拿 Espresso 那一套测 Compose,应该也差不多吧?」结果一上手就发现,完全不是一回事。
这一章我们先聊聊 Compose 测试框架到底是什么,它跟传统的 View 系统测试到底差在哪。
1.1 Compose 测试框架简介
Compose 测试的核心依赖是 compose-ui-test 和 compose-ui-test-junit4。你别看名字长,其实用起来就三步:添加依赖、写测试用例、运行。我习惯把 Compose 测试分成两类:
- 语义树测试:通过组件的语义属性(如标签、状态描述)来定位和验证。
- 节点树测试:通过组件在 UI 树中的精确位置或类型来找节点,适合更底层的校验。
一个最简单的 Compose 测试长这样:
@RunWith(AndroidJUnit4::class)
class MyComposeTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testButtonClick() {
composeTestRule.setContent {
MyScreen()
}
// 找一个带文本 "确认" 的按钮
composeTestRule.onNodeWithText("确认").performClick()
// 验证弹窗是否出现
composeTestRule.onNodeWithText("操作成功").assertIsDisplayed()
}
}
这里面最关键的就是 composeTestRule。它替代了传统的 ActivityScenario 或 ActivityTestRule。为什么?因为 Compose 不依赖 Activity 的布局文件,它直接在 Composable 函数里描述 UI。测试时我们只需 setContent 传入一个 Composable,就能构造出被测界面,不需要启动一个完整的 Activity。
createAndroidComposeRule<ComponentActivity>(),它能让你拿到宿主 Activity 的 context,方便用 Intent 跳转或者弹 Dialog。
1.2 与传统 View 测试的核心区别
为什么说 Compose 测试和传统 View 测试是两套思维?我给你列个对比表,一看就清楚:
| 维度 | 传统 View + Espresso | Compose + ComposeTestRule |
|---|---|---|
| UI 布局方式 | XML 布局文件,通过 findViewById / 注解绑定 | Kotlin 函数(Composable),无文件绑定 |
| 节点查找机制 | 基于 ViewHierarchy,通过 id / text / contentDescription | 基于语义树 + 节点树,通过文本 / 语义标签 / 标签类型 |
| 测试规则 | ActivityTestRule / ActivityScenario | ComposeTestRule / AndroidComposeTestRule |
| 等待机制 | IdlingResource 控制空闲状态 | 自动等待动画完成(可配置) |
| 断言方式 | ViewAssertions (matches, isDisplayed 等) | SemanticsMatcher + 扩展函数 (assertIsDisplayed 等) |
| 异步支持 | 需要手动封装 IdlingResource | 内置等待 recomposition 完成 |
说白了,传统测试里你要面对的是一个 View 层次树,而 Compose 测试面对的是「语义语义树」。后者更关注「组件想要表达什么」,而不是「组件在哪个位置」。比如一个按钮,传统测试可能靠它的 id 或 text 来定位,而 Compose 更倾向于用它的语义描述(比如「关闭按钮」、「确认按钮」)。
我曾在项目里把一段复杂的 RecyclerView 迁移到 LazyColumn,对应的测试从 Espresso 换成了 ComposeTestRule。一开始我还在用 onView(withId(R.id.item)) 那套思路,结果发现 compose 里压根没有 withId,得用 onNodeWithTag 或者传一个 testTag。说实话,当时踩了不少坑。
testTag 必须通过 Modifier.testTag("xxx") 主动设置。如果漏了,你很可能找不到节点。我建议在写组件时就给关键交互控件加上 testTag,别等测试时再补,不然你会后悔的。
1.3 为什么要学 Compose 测试?
你想想看,现在新项目大多用 Compose 写界面,如果测试还停留在 Espresso 那一套,遇到 Compose 组件就完全测不了——因为 Espresso 找不到 Compose 节点。反过来,ComposeTestRule 也能测部分传统 View(比如 WebView、SurfaceView),但体验不太好。所以实际项目里往往是二者共存:Compose 界面用 ComposeTestRule,遗留 View 页面用 Espresso。
这也是我设计这门课的初衷——让 Compose 测试和 Espresso 测试能够在回归流程里协同工作,而不是互相对立。接下来我们会逐步深入,从基础写法到混合场景,再到 CI 集成,一步步来。
1.4 知识体系总览
下面这张图概括了本章的核心脉络,也是整门课的顶层结构:
从这张图可以看到,我们的学习路径是从「Compose 测试框架是什么」出发,然后对比它和传统 View 测试的差异,最后落到「如何让两者在回归测试中协同工作」。后面每一章都会围绕这些节点展开。
onNodeWithText、onNodeWithTag 和 assertIsDisplayed 这三个核心 API,能覆盖 80% 的场景。剩下的 20% 再学 onChildren、assertTextEquals 等进阶操作。