Appearance
移动端测试能力总览:单测、Widget/UI 测试与集成测试金字塔
回到总览:性能、测试、排障
相关模块:性能优化总览与移动端性能四要素 · 系统化排障方法论:现象 → 复现 → 数据 → 定位 → 验证
一句话定义
测试能力总览是指围绕测试金字塔(Testing Pyramid) 理论,将移动端测试划分为底层的单元测试(Unit Test)、中层的组件/UI 测试(Widget / Instrumentation Test)与顶层的端到端集成测试(E2E Integration Test),以最低的维护成本换取最高代码质量的工程治理体系。
代码索引
计划补齐的实验:
labs/android/host/topics/testing/testing-overview-demo/— 单元测试(Mockito/Robolectric)与 UI 仪器测试(Espresso)分层 Demo
为什么需要
- 为什么仅靠 UI 自动化测试(如 Appium / UI Automator)无法构建高效的质量防护网?
- 一句话答:UI 自动化测试执行极慢、极易受网络和设备环境波动影响(Flaky Test,不稳定测试),且维护成本极高;只有按照金字塔结构将 70% 的逻辑收口在执行毫秒级且极其稳定的单元测试中,才能实现高效的质量反馈。
- 为什么 10 年 Android 工程师必须掌握测试分层与 Mock 隔离?
- 一句话答:良好的可测试性(Testability)是代码架构设计优秀的试金石;如果一个 ViewModel 或 Class 难以编写单测,通常意味着其违反了单一职责原则或强耦合了系统 Context,驱动架构向依赖注入(DI)和分层解耦演进。
- 为什么 Robolectric 在 Android 单元测试中被广泛替代真机测试?
- 一句话答:Robolectric 在纯 JVM 环境下通过 Shadow 机制摹拟了 Android SDK(如 Context、Looper、View),使得依赖 Android SDK 的测试用例无需启动真机或模拟器即可在电脑 JVM 上秒级跑完。
底层机制
1. 测试金字塔 (Testing Pyramid) 结构
图示说明:① 单元测试(Unit Tests,JVM 纯跑,毫秒级)→ ② 组件/UI 测试(Component Tests,Robolectric/模拟器,秒级)→ ③ 端到端集成测试(E2E Integration Tests,真机/云真机,分钟级)。金字塔越底层,覆盖量越大、执行越快、维护越低。
三层测试对比表
| 层级 | 运行环境 | 执行速度 | 维护成本 | 主要验证内容 |
|---|---|---|---|---|
| 单元测试 (Unit) | 纯 JVM / Dart VM (无需设备) | 毫秒级 | 极低 | 业务逻辑算法、ViewModel、数据解析 |
| 组件/UI 测试 | Robolectric / Android 模拟器 | 秒级 | 中等 | Component 重组、View 绘制、页面交互 |
| 集成测试 (E2E) | 真实物理设备 / 云真机 | 分钟级 | 极高 | 完整端到端链路、网络与数据库交互 |
Android / Flutter / Web / Backend 对照
| 测试框架 | Android Native | Flutter 框架 | Web 前端 |
|---|---|---|---|
| 单元测试 | JUnit 5 + MockK / Mockito | flutter_test + mockito | Jest / Vitest |
| 组件/UI 测试 | Espresso / Robolectric | testWidgets (WidgetTester) | React Testing Library |
| 集成测试 | UI Automator / Appium | integration_test 插件 | Cypress / Playwright |
常见场景
1. ViewModel 单元测试范式 (Kotlin + MockK + Coroutines)
kotlin
class UserViewModelTest {
private val repository = mockk<UserRepository>()
private lateinit var viewModel: UserViewModel
@Test
fun `loadUser success should update state`() = runTest {
// 1. Arrange (准备 Mock 条件)
coEvery { repository.getUser("10086") } returns User("10086", "Alex")
// 2. Act (执行被测方法)
viewModel = UserViewModel(repository)
viewModel.loadUser("10086")
// 3. Assert (验证结果)
assertEquals("Alex", viewModel.uiState.value.user?.nickname)
coVerify(exactly = 1) { repository.getUser("10086") }
}
}常见误配、事故后果与排障
1. 事故:把"单测通过"当成"功能可用"
- 误配原因:只写了 ViewModel 的 happy path 单测(
coEvery全部 mock 成功路径),断言通过就宣布功能完成。 - 后果:真实网络失败、空数据、超时等分支全未覆盖,线上首次遇到异常直接崩;且 mock 的
repository行为与真实实现脱节(mock 通过 ≠ 集成正确)。 - 排障与修法:单测必须覆盖成功/失败/空/超时等分支(参数化测试);同时补一层组件测试或契约测试,让 mock 与真实接口契约对齐。
2. 事故:E2E 测试写成"全量回归",跑一次要 1 小时
- 误配原因:把 70% 的验证都放在真机 E2E 上,金字塔倒置。
- 后果:测试慢 → 没人跑 → 回归失效;且 E2E 因网络/环境波动 Flaky,误报率极高。
- 排障与修法:把稳定逻辑下沉到单测(毫秒级),E2E 只保留"关键用户旅程"(登录 → 主流程 → 退出)等少量高价值场景,并做重试与稳定性治理。
对应实验
计划补齐的实验:
labs/android/host/topics/testing/testing-overview-demo/— 单元测试(Mockito/Robolectric)与 UI 仪器测试(Espresso)分层 Demo
复习检查题
在单元测试中,为什么要求测试用例必须具备“隔离性 (Isolation)”和“可重复性 (Repeatability)”?
答:因为单元测试的目的是验证单个独立逻辑单元的正确性。如果单测依赖真实的外部网络 API、数据库或真实系统时间,测试结果就会受到网络波动、数据库残余数据或时区的影响,产生时不时报错的不稳定测试(Flaky Test)。通过 Mock/Stub 隔离外部依赖,能保证测试在任何环境、任何时间运行结果都 100% 稳定一致。
为什么说“代码的可测试性(Testability)”是评估架构质量的重要指标?
答:如果一段代码难以编写单元测试,通常是因为该代码内部存在严重的架构缺陷,例如:滥用单例/静态全局变量、高耦合了 Android 的
Context或Activity、违反了依赖倒置原则(Direct Dependency)导致无法进行 Dependency Injection(依赖注入)。易于编写单测的代码必定是高内聚、低耦合、职责单一的优秀架构。
速记
- 金字塔法则:70% 纯 JVM 单测,20% 组件 UI 测试,10% 端到端集成测试。
- 单测三步曲:AAA 范式(Arrange 准备 Mock → Act 执行被测 → Assert 校验断言)。
- 可测试性:单测是架构解耦的试金石,隔离外部网络与数据库依赖。