一、测试思维与TDD哲学:为什么需要单元测试?
说实话,我刚开始做Android开发那几年,对单元测试是有点抵触的。总觉得「写代码都来不及,还写测试?」。直到有一次,我负责的一个支付模块上线后出了严重bug,导致用户重复扣款。那次事故让我意识到——没有测试的代码,就像在悬崖边跳舞。
今天这一章,我想跟你聊聊测试思维和TDD哲学。这不是什么高大上的理论,而是我踩过无数坑之后总结出来的实战经验。
1.1 为什么需要单元测试?
先问个问题:你写代码的时候,有没有遇到过这种情况?
- 改了一行代码,结果另一个模块崩了
- 上线前信心满满,上线后bug不断
- 重构代码时,根本不敢动,怕改出问题
嗯,这些我都经历过。单元测试就是来解决这些问题的。
单元测试的核心价值,说白了就三点:
- 快速反馈:写完代码立刻知道对不对,不用等编译、安装、跑流程
- 安全重构:有测试兜底,改代码时心里有底
- 文档即代码:测试用例就是最好的使用文档,告诉你这个函数该怎么用
我个人习惯:每次提交代码前,先跑一遍单元测试。如果测试没过,绝不提交。这个习惯帮我避免了很多线上事故。
1.2 TDD的红-绿-重构循环
TDD,全称是Test-Driven Development,测试驱动开发。它的核心流程就三个步骤:
红(Red) → 绿(Green) → 重构(Refactor)
听起来很简单对吧?但真正做起来,很多人会走偏。
第一步:红(Red)
先写一个会失败的测试。注意,是先写测试,再写代码。这个测试描述了你期望的行为。
// 先写测试
@Test
fun `should return true when password is valid`() {
val validator = PasswordValidator()
val result = validator.isValid("Abc123!@#")
assertTrue(result)
}
这时候运行测试,肯定是红色的——因为PasswordValidator类还不存在。
第二步:绿(Green)
写最少的代码让测试通过。不要想太多,够用就行。
class PasswordValidator {
fun isValid(password: String): Boolean {
return password.length >= 8
}
}
运行测试,变绿了。好,第一步完成。
第三步:重构(Refactor)
现在测试通过了,可以优化代码了。但记住:重构时不能改变外部行为。
class PasswordValidator {
fun isValid(password: String): Boolean {
return password.length >= 8 &&
password.any { it.isUpperCase() } &&
password.any { it.isDigit() }
}
}
重构完再跑测试,还是绿的。完美。
避坑指南:我曾经犯过一个错误——在「绿」阶段写了很多额外功能。结果测试虽然过了,但代码变得很复杂。记住:只写让当前测试通过的最小代码。
1.3 测试金字塔模型
你想想看,如果所有测试都写成端到端测试,会怎样?
跑一次测试要几分钟,甚至几十分钟。而且一旦某个环节出问题,整个测试就挂了,你根本不知道是哪里错了。
这就是为什么我们需要测试金字塔。
| 层级 | 类型 | 速度 | 数量 | 维护成本 |
|---|---|---|---|---|
| 顶层 | 端到端测试(E2E) | 慢(分钟级) | 少(5-10%) | 高 |
| 中间层 | 集成测试 | 中等(秒级) | 适中(15-20%) | 中 |
| 底层 | 单元测试 | 快(毫秒级) | 多(70-80%) | 低 |
说白了,测试金字塔告诉我们:大部分测试应该是单元测试。因为它们快、稳定、容易维护。
我在项目中遇到过这样的情况:团队写了大量UI测试,结果每次UI改动都要改测试,维护成本极高。后来我们把重心移到单元测试上,测试覆盖率从30%提升到85%,而且跑一次全量测试从20分钟降到了3分钟。
1.4 Android测试的分类与定位
Android测试主要分三类:
- 单元测试(Unit Test):测试单个类或方法,不依赖Android框架。运行在JVM上,速度最快。
- 集成测试(Integration Test):测试多个组件之间的交互,比如ViewModel+Repository。可能需要模拟Android环境。
- UI测试(UI Test):测试用户界面行为,比如点击按钮、输入文本。运行在模拟器或真机上,速度最慢。
那它们各自应该放在哪里?
// 单元测试:放在 src/test 目录下
// 运行在JVM,不需要模拟器
class LoginViewModelTest {
@Test
fun `login with valid credentials should succeed`() {
// ...
}
}
// UI测试:放在 src/androidTest 目录下
// 需要模拟器或真机
class LoginActivityTest {
@Test
fun `click login button should navigate to home`() {
// ...
}
}
注意:不要把UI测试当成单元测试来写。我见过有人用Espresso测试一个简单的数据解析逻辑,结果测试跑一次要30秒。这种逻辑用单元测试,10毫秒就能搞定。
1.5 我的建议
如果你是刚开始接触测试,我建议你:
- 从单元测试开始:先给工具类、数据解析、业务逻辑写测试
- 坚持TDD:哪怕只是一个小功能,也试试先写测试再写代码
- 不要追求100%覆盖率:80%的覆盖率比100%但全是无效测试要好得多
嗯,这一章就到这里。下一章我们会动手写第一个单元测试,到时候你会真正感受到TDD的魅力。
记住:测试不是负担,而是你的安全网。有了它,你才能放心地重构、大胆地迭代。