单元测试与集成测试:分清楚它们,少走一半弯路
做 Compose 重组测试,第一个要搞明白的就是两种测试层次:单元测试和集成测试。我刚开始接触时,觉得不都是跑一下用例吗?后来才发现,选错了测试层次,要么跑得慢得要命,要么根本测不出真正的问题。今天咱们就彻底聊清楚它们的区别、适用场景和优劣。
单元测试 —— 切到最细,测到骨头里
单元测试,说白了就是只测一个最小的、独立的功能单元。在 Compose 里,这个“单元”可以是一个 Composable 函数、一个 ViewModel 中的方法、一个状态计算逻辑。它不依赖任何外部环境——不碰数据库、不碰网络、不碰其他组件。
优点:
- 执行飞快——毫秒级跑完几千个用例
- 定位问题精准——失败时你基本知道是哪一行代码错了
- 容易维护——依赖少,改动成本低
缺点:
- 无法验证组件之间的交互
- 对 UI 渲染结果的检查比较浅——只能测到是否存在某个文本,测不到布局是否对齐
- 容易写出“为了测试而测试”的代码,背离真实场景
举个例子,我写过这样的单元测试:
@Test
fun testGreeting() {
composeTestRule.setContent {
Greeting(name = "World")
}
composeTestRule.onNodeWithText("Hello, World!").assertExists()
}
这段代码只测 Greeting 这个 Composable 里有没有显示出指定的字符串。速度快,但一旦这个组件需要从网络拿数据,它就无能为力了。嗯,这里要注意:千万别为了追求单元测试的覆盖率,把本该集成测试的东西硬塞进来。
集成测试 —— 多模块联调,还原真实玩法
集成测试则是把多个单元串起来测。比如一个列表页面,它要从 ViewModel 拿数据、显示加载状态、处理点击事件、跳转详情页——这些全部一起测。它会启动真实的 Activity(或 Compose 的 TestHost),模拟用户操作。
优点:
- 最贴近真实用户行为——能发现组件间接口不匹配、状态传递丢失等问题
- 能测到 UI 布局、动画、手势等复杂行为
- 覆盖度高——一次测试可能同时覆盖 View、ViewModel、Repository
缺点:
- 慢——跑一个集成测试可能要几秒甚至十几秒
- 定位问题困难——失败时你要从一堆栈里扒原因
- 环境依赖重——需要 Mock 网络请求、文件系统等
说实话,我团队里曾经有一半的测试是集成测试,跑一次全量测试要 40 分钟……后来我们把纯业务逻辑拆成了单元测试,集成测试只保留核心流程,速度直接降到 8 分钟。所以这两种测试不是谁替代谁,而是各有各的位置。
一张对比表,看清本质
| 对比维度 | 单元测试 | 集成测试 |
|---|---|---|
| 测试范围 | 单个函数或 Composable | 多个组件 + 真实依赖(或 Mock) |
| 执行速度 | 毫秒级 | 秒级甚至分钟级 |
| 调试难度 | 低,失败即定位 | 高,需要追踪调用链 |
| 环境依赖 | 无 | 需要 Mock 或真实后端 |
| 重构敏感性 | 低,重构通常不破测试 | 高,重构可能大面积挂掉 |
| 典型代表 | 纯逻辑、状态计算、单组件渲染 | 页面流程、导航、数据交互 |
为什么你不应该只写一种测试?
你想想看,如果你只有单元测试,一个页面改了什么你不一定知道——可能几个 Composable 之间的状态传递断了,单元测试根本发现不了。反过来,如果只有集成测试,每次改一行代码可能都要跑 10 分钟才知道结果。
我曾经吃过一个亏:对一个电商列表页只写了集成测试。后来要加一个商品标签筛选功能,我改了 ViewModel 的过滤逻辑,集成测试全绿,结果上线后列表不刷新——因为 Compose 重组时状态没有触发重组。如果我们有个单元测试去单独验证重组逻辑,早就发现了。嗯,从那以后我坚持任何状态变化逻辑都要写单元测试。
实际项目中的分层策略
我建议按这个金字塔结构来组织测试:
- 底层(单元测试):ViewModel 的普通函数、纯计算类、单个 Composable 的渲染用例。用
composeTestRule配合semantics做节点断言。 - 中间层(集成测试):一个页面的完整流程,如点击按钮→加载数据→显示列表→点击 item→跳转。用
createComposeRule()或AndroidComposeTestRule启动真实 Activity。 - 顶层(端到端测试):跨页面的用户流程,比如登录→首页→搜索→下单。用
ComposeTestRule配合Espresso模拟真实设备操作。
Thread.sleep 等硬等待。应该使用 composeTestRule.waitUntil 或协程的 runBlockingTest。硬等待会让测试变得不稳定,而且慢。
好了,现在你对单元测试和集成测试有了清晰的分界线。下一件重要的事是学怎么用 Mock 来模拟这些外部依赖——那就是 Mock 回放方法的用武之地了。