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。

💡 一个小技巧: 如果你需要在测试中模拟多个 Composable 组合,可以用 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。说实话,当时踩了不少坑。

⚠️ 注意: Compose 的 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 测试知识体系总览 Compose 测试概述 Compose 测试框架 与传统 View 比较 协同回归实践目标 语义树测试 节点树测试 布局方式不同 节点定位策略 Compose + Espresso 回归流程集成 ⚠️ 核心要点 学会区别语义树与 View 树,才能在两者间灵活切换测试策略。 后续章节:ComposeTestRule 实战与 Espresso 混合用法

从这张图可以看到,我们的学习路径是从「Compose 测试框架是什么」出发,然后对比它和传统 View 测试的差异,最后落到「如何让两者在回归测试中协同工作」。后面每一章都会围绕这些节点展开。

📌 我的建议: 刚开始接触 Compose 测试,别急着写复杂用例。先掌握 onNodeWithText、onNodeWithTag 和 assertIsDisplayed 这三个核心 API,能覆盖 80% 的场景。剩下的 20% 再学 onChildren、assertTextEquals 等进阶操作。