第七篇:进阶篇 —— 工程化与质量保障
第14章 自动化测试:构筑代码质量的防火墙
在第十三章,我们通过 Profiler 优化了 App 的性能。但如果我们改了一行代码,修复了一个 Bug,却不小心引入了三个新 Bug 怎么办?靠人工点点点测试,在 2026 年是低效且不可靠的。
自动化测试是专业开发者的护城河。它是一张安全网,让你敢于重构代码、升级依赖、添加新功能,而无需担心把 App 搞崩。
本章将带你构建三层测试体系:单元测试(Unit Test)、集成测试(Integration Test)和UI 测试(UI Test)。我们将使用 2026 年最新的测试栈:JUnit 5 + MockK + Turbine + Compose UI Test。我们将深入测试的哲学、Mock 的底层原理、以及如何测试 Kotlin 协程和 Flow。
14.1 测试金字塔:你应该写哪些测试?(深度解析)
并不是测试越多越好,而是要分层。
| 测试类型 | 占比 | 速度 | 成本 | 关注点 | 2026年工具栈 |
|---|---|---|---|---|---|
| UI 测试 | 10% | 慢 | 高 | 模拟用户操作,验证界面流程(如:点击登录按钮是否跳转到主页)。 | Compose UI Test |
| 集成测试 | 20% | 中 | 中 | 验证模块协作(如:ViewModel 调用 Repository 是否能拿到数据)。 | Hilt + Robolectric |
| 单元测试 | 70% | 极快 | 低 | 验证最小功能单元(如:一个工具函数、一个 ViewModel 的状态计算)。 | JUnit 5 + MockK + Turbine |
核心原则:测试应该是快速的。如果你跑一次测试需要 5 分钟,没人会跑。
14.1.1 为什么是 70/20/10?
- 单元测试(70%):最便宜,最快。它们不依赖 Android 框架,直接在 JVM 上运行。你应该为每一个业务逻辑类编写单元测试。
- 集成测试(20%):验证组件之间的交互。例如,ViewModel 是否正确调用了 Repository,数据库是否正确保存了数据。
- UI 测试(10%):最昂贵,最慢。它们运行在真机或模拟器上,模拟真实用户操作。只为核心业务流程编写 UI 测试。
14.2 单元测试:JUnit 5 与 MockK(深度解析)
单元测试是针对纯 Kotlin 代码的测试,不依赖 Android 设备,直接在 JVM 上运行。
14.2.1 配置测试环境(深度解析)
// build.gradle.kts (App)dependencies{// JUnit 5 (Jupiter)testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")testRuntimeOnly("org.junit.platform:junit-platform-launcher")// MockK (Kotlin 专用 Mock 框架,替代 Mockito)testImplementation("io.mockk:mockk:1.13.9")// Turbine (测试 Flow 的神器)testImplementation("app.cash.turbine:turbine:1.0.0")// 测试协程testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0")}14.2.2 JUnit 5 注解详解(深度解析)
| 注解 | 作用 | 示例 |
|---|---|---|
@Test | 标记一个测试方法 | @Test fun addition_isCorrect() |
@BeforeEach | 每个测试方法前执行 | @BeforeEach fun setup() |
@AfterEach | 每个测试方法后执行 | @AfterEach fun tearDown() |
@BeforeAll | 所有测试方法前执行一次 | @BeforeAll companion object { ... } |
@AfterAll | 所有测试方法后执行一次 | @AfterAll companion object { ... } |
@DisplayName | 测试显示名称 | @DisplayName("加法测试") |
@Nested | 嵌套测试类 | @Nested inner class AdditionTests |
@Timeout | 超时测试 | @Timeout(1, unit = TimeUnit.SECONDS) |
14.2.3 测试一个简单的工具类(深度实战)
假设我们有一个StringUtils.kt:
// utils/StringUtils.ktobjectStringUtils{funisValidEmail(email:String):Boolean{returnemail.contains("@")&&email.contains(".")}funformatPrice(price:Double):String{return"¥${String.format("%.2f",price)}"}}编写测试:
// test/java/com/example/myfirstapp/utils/StringUtilsTest.ktimportorg.junit.jupiter.api.Assertions.assertFalseimportorg.junit.jupiter.api.Assertions.assertTrueimportorg.junit.jupiter.api.DisplayNameimportorg.junit.jupiter.api.TestclassStringUtilsTest{@Test@DisplayName("Valid email should return true")fun`isValidEmail returns true for valid email`(){// Givenvalemail="test@example.com"// Whenvalresult=StringUtils.isValidEmail(email)// ThenassertTrue(result)