测试体系
★ 测试体系是中级 Android 面试的短板高频区。能讲清“怎么测“,比只会说 MVVM/MVI 更能证明工程能力。
一、测试金字塔与 Android 测试分层
| 层级 | 目标 | 工具 | 典型对象 |
|---|---|---|---|
| 单元测试 | 快速验证纯逻辑 | JUnit / Truth / MockK | UseCase、Repository、Reducer |
| 集成测试 | 验证多层协作 | Robolectric / fake data source | ViewModel + Repository |
| UI 测试 | 验证用户路径 | Espresso / Compose Test | 页面交互、导航、错误提示 |
原则:越靠下越多、越快、越稳定;越靠上越少、越接近真实用户。
二、单元测试:Junit、断言与 Mock
- JUnit 负责组织测试生命周期。
- Truth/AssertJ 让断言可读。
- MockK/Mockito 用于隔离外部依赖,但不要 mock 一切。
class LoginUseCaseTest {
@Test
fun `blank username returns validation error`() {
val useCase = LoginUseCase(fakeRepository)
val result = useCase.execute(username = "", password = "123456")
assertThat(result).isEqualTo(LoginResult.InvalidUsername)
}
}
三、协程与 Flow 测试
- 用
runTest控制虚拟时间。 - 用
StandardTestDispatcher替换真实 dispatcher。 - Flow 可用 Turbine 验证 emit 顺序。
@Test
fun `flow emits loading then success`() = runTest {
repository.userFlow().test {
assertThat(awaitItem()).isEqualTo(UiState.Loading)
assertThat(awaitItem()).isInstanceOf(UiState.Success::class.java)
awaitComplete()
}
}
四、ViewModel / Repository 怎么测
ViewModel 测试重点不是测试 Android 框架,而是测试输入事件到 UI State 的转换。
| 对象 | 测什么 | 不测什么 |
|---|---|---|
| ViewModel | state/effect、错误处理、重试 | 具体控件绘制 |
| Repository | 缓存策略、数据源切换 | Retrofit/Room 本身 |
| UseCase | 业务规则 | 外部 IO |
五、UI 测试:Espresso 与 Compose Test
- Espresso 适合 View 体系页面。
- Compose Test 适合声明式 UI,优先通过 semantic matcher 找节点。
- UI 测试要覆盖核心路径,不要把所有边界都堆在 UI 层。
六、可测试性如何反推架构质量
如果一个 ViewModel 很难测,通常说明它持有太多 Android 依赖、业务逻辑没有下沉、状态/副作用没有分离。
高频面试题
Q1:你项目里怎么做测试分层? 答:核心业务规则放单元测试,ViewModel/Repository 做集成测试,关键用户路径做 UI 测试。比例上单元测试最多,UI 测试最少。
Q2:为什么 Repository 不应该直接测 Retrofit/Room? 答:Retrofit/Room 是框架能力,业务测试应关注 Repository 的缓存、错误兜底、数据源切换。框架集成可少量用 integration test 验证。
Q3:协程测试为什么不用真实 delay?
答:真实 delay 会让测试慢且不稳定;runTest 用虚拟时间推进,可稳定验证超时、重试、debounce 等逻辑。
易错点 / 追问
- 不要为了覆盖率 mock 所有东西,那会测到实现细节。
- UI 测试不要依赖真实网络和随机数据。
- Compose 测试要给关键节点加稳定语义,否则 matcher 容易脆弱。