第1章 开篇:为什么要学混合测试?
说实话,我刚接触 Android 测试那会儿,觉得 Espresso 就已经够用了。后来项目慢慢迁移到 Compose,问题就来了——老页面用 Espresso,新页面用 Compose,测试到底该怎么写?
你想想看,一个 App 里既有 View 系统又有 Compose,如果测试只能测一边,那另一半的 UI 就等于裸奔。所以我个人觉得,搞懂混合测试已经不是「进阶技巧」,而是每个 Android 工程师早晚要迈过去的坎。
从测试金字塔说起
测试金字塔这个概念,大家应该不陌生。我习惯把它理解成「投入产出比」的指南:底层单元测试快、便宜、好维护,适合写大量;顶层 E2E 测试慢、贵、不稳定,只能写关键路径。
但在 Android 里,UI 测试其实横跨了中间两层:集成测试和 E2E。你写个 Espresso 测一个 Activity 里的几个 View,这算集成测试;要是启动 App 然后测完整登录流程,那就算 E2E。说白了,工具一样,测试范围不同而已。
混合测试的核心问题:当你的 App 里既有 View 又有 Compose,测试范围该怎么切分?我曾经在一个页面上同时用 RecyclerView(View 系统) 和一段 Compose 的 BottomSheet,Espresso 拿不到 Compose 节点,Compose 的测试规则又没法直接操作 RecyclerView。嗯,这就是我们这门课要解决的核心痛点。
Espresso 与 Compose——两套体系
先列个表,方便你一眼看出差异:
| 对比维度 | Espresso (View 系统) | Compose UI 测试 |
|---|---|---|
| 测试规则 | ActivityTestRule / ActivityScenario | ComposeTestRule (createAndroidComposeRule) |
| 找节点方式 | onView(withId(R.id.foo)) | onNodeWithTag("foo") / onNodeWithText(...) |
| 执行交互 | perform(click()) | performClick() |
| 跨组件限制 | 无法直接操作 Compose 节点 | 无法直接操作 View 系统的子节点 |
| 官方推荐场景 | 传统 View 页面 / 已有项目 | Compose 新功能 / 纯 Compose 页面 |
ActivityScenario + ComposeTestRule 双规则,虽然配置要小心,但维护成本低了一半。
你可能会问:「为什么不能只用一种?」原因很简单——Compose 和 View 系统的渲染树是两棵独立的树。Espresso 的 onView 只搜 View 树,Compose 的 onNode 只搜 Compose 树。两个世界中间隔着一堵墙。
混合测试的价值与挑战
说白了,价值就一句话:覆盖你 App 的真实 UI 场景。一个页面里既有 View 又有 Compose,你只测一边,另一边只是「看起来跑过了」,实际上可能按钮点了没反应、列表滚动冲突。我遇到过几次线上崩溃,都是因为 View 和 Compose 之间的消息传递没测到。
那挑战呢?我总结成三点:
- 测试规则冲突:同一个测试方法里同时准备
ActivityScenario和ComposeTestRule,容易导致生命周期混乱。 - 节点定位困难:Espresso 找不到 Compose 节点,Compose 找不到 View 节点。怎么办?要么切换规则,要么用
ComposeTestRule的onView桥接——但桥接有性能损耗。 - 等待机制差异:Espresso 自带 IdlingResource,Compose 有
waitForIdle(),混在一起时往往出现「一个等了,另一个没等」的偶发失败。
onView 和 onNode,没有显式设置等待,结果 CI 上 30% 的用例偶尔失败。排查了一天才发现:Espresso 的 perform 会等主线程空闲,但 Compose 动画完成后才真正空闲。解决办法是在 Compose 操作后加 composeTestRule.waitForIdle(),没有更好的捷径。
本章知识结构图
小结
这一章我们聊了测试金字塔的分层、两套测试体系的差异,以及混合测试为什么既诱人又麻烦。我个人觉得,最重要的不是你今天记住多少 API,而是建立「混合视角」——以后看到一个新页面,先问自己:这里哪部分是 View,哪部分是 Compose?然后决定测试策略。
嗯,后面几章我们会动手写代码,从最简单的单页面混合测试开始,一步步搞定双规则、异步等待、跨组件定位这些硬骨头。你准备好了吗?