1. 为什么单元测试值得你投入时间?
刚入行时我也觉得写单元测试纯粹是浪费时间——直到那次线上事故让我连续加班三天。那次因为一个简单的数值计算函数没做边界值测试,导致凌晨两点整个支付系统崩溃。从那天起,我真正理解了单元测试不是给机器写的,而是给未来的自己买的保险。
好的单元测试能让你:
- 在修改代码时像有安全网的高空作业(我称之为"开发者的蹦极绳")
- 在重构时获得即时反馈(比产品经理的夺命连环call更友好)
- 减少80%的低级错误(特别是周五下午写的代码)
2. 单元测试的黄金四要素
2.1 独立性:每个测试都是孤岛
我见过最糟糕的测试是需要在本地数据库插入特定数据才能运行的案例。记住:
- 使用Mock代替真实数据库(推荐Mockito)
- 避免测试间的状态依赖
- 示例:测试订单服务时,应该这样隔离依赖:
java复制@Test
void should_calculate_total_price() {
// 创建Mock对象
ProductRepository mockRepo = mock(ProductRepository.class);
// 定义Mock行为
when(mockRepo.findById(1L)).thenReturn(new Product(1L, "iPhone", 5999.00));
OrderService service = new OrderService(mockRepo);
double total = service.calculateTotal(Collections.singletonList(1L));
assertEquals(5999.00, total);
}
2.2 可重复性:每次执行结果必须一致
踩过的坑:测试依赖系统当前时间,在跨时区CI服务器上随机失败。解决方案:
- 固定时间源(比如用Java的Clock类)
- 避免使用随机数(除非是测试随机逻辑本身)
- 清理测试产生的临时文件
2.3 原子性:一个测试只验证一件事
反例:
javascript复制// 错误示范:测试耦合了多个断言
it('should process user registration', () => {
const result = registerUser('test@example.com', '123456');
expect(result.success).toBe(true);
expect(result.user.email).toBe('test@example.com');
expect(result.user.role).toBe('member');
});
应该拆分成三个独立测试,每个只关注一个行为点。
2.4 可读性:测试是最好的文档
我团队的规矩:测试方法名必须用should_xxx_when_yyy格式:
python复制def test_should_raise_error_when_amount_exceeds_balance():
account = Account(balance=100)
with pytest.raises(InsufficientBalanceError):
account.withdraw(200)
3. 实战中的测试策略
3.1 测试金字塔:别在单元测试里做集成测试
常见误区:把Spring上下文加载、数据库连接都塞进单元测试。记住比例:
- 单元测试:70%(快速反馈)
- 集成测试:20%(模块间交互)
- E2E测试:10%(完整流程)
3.2 边界值分析的魔法
对于数值处理,永远测试这些case:
- 最小值-1
- 最小值
- 正常值
- 最大值
- 最大值+1
- 空值/null
比如测试分页逻辑:
typescript复制describe('pagination', () => {
it('should return first page when page=1', () => {...});
it('should throw error when page=0', () => {...});
it('should return empty array when page=999', () => {...});
});
3.3 测试替身的选择指南
| 替身类型 | 适用场景 | 典型工具 |
|---|---|---|
| Dummy | 需要但不使用的参数 | 手动创建 |
| Stub | 返回预设值 | Mockito, Sinon |
| Spy | 记录调用信息 | Jest spyOn |
| Mock | 验证交互行为 | Mockito, unittest |
| Fake | 轻量级实现 | 内存数据库 |
4. Vue组件测试特别指南
4.1 组件测试三大原则
- 只测试公开接口(props/events)
- 避免测试实现细节(比如内部方法名变更不应导致测试失败)
- 使用浅渲染(shallowMount)隔离子组件
4.2 解决常见报错
Case 1: "Cannot read property '$store' of undefined"
解决方案:
javascript复制import { createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
const localVue = createLocalVue()
localVue.use(Vuex)
const store = new Vuex.Store({...})
const wrapper = shallowMount(Component, { localVue, store })
Case 2: "window.matchMedia is not a function"
在测试文件头部添加:
javascript复制window.matchMedia = jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn()
}));
5. 测试代码的DRY原则
5.1 工厂函数代替重复代码
坏味道:
java复制@Test
void test1() {
User user = new User("test1", "user1@test.com", "123456");
// ...
}
@Test
void test2() {
User user = new User("test2", "user2@test.com", "654321");
// ...
}
改进方案:
java复制class UserTestDataFactory {
static User createValidUser() {
return new User("defaultName", "default@test.com", "defaultPass");
}
static User createUserWithEmail(String email) {
return new User("defaultName", email, "defaultPass");
}
}
5.2 参数化测试的艺术
JUnit 5示例:
java复制@ParameterizedTest
@CsvSource({
"2, 2, 4",
"3, 5, 8",
"10, -5, 5"
})
void testAdd(int a, int b, int expected) {
assertEquals(expected, Calculator.add(a, b));
}
6. 测试覆盖率陷阱
不要盲目追求100%覆盖率,关注:
- 核心业务逻辑必须全覆盖
- getter/setter可以忽略
- 异常处理分支要重点覆盖
使用JaCoCo配置排除不重要的包:
xml复制<configuration>
<excludes>
<exclude>**/model/*</exclude>
<exclude>**/dto/*</exclude>
</excludes>
</configuration>
7. 测试性能优化技巧
- 使用@BeforeAll代替@BeforeEach初始化昂贵资源
- 避免在循环中创建测试数据
- 对于慢速测试,添加@Tag("slow")分组执行
我的CI配置示例:
yaml复制jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Run fast tests
run: mvn test -Dgroups=!slow
- name: Run slow tests
run: mvn test -Dgroups=slow
8. 测试驱动开发(TDD)实战
8.1 红-绿-重构循环
- 先写一个必定失败的测试(红)
- 用最快的方式让测试通过(绿)
- 优化代码结构(重构)
8.2 TDD常见误区
- 把测试写成验证脚本(应该描述行为)
- 跳过重构步骤(技术债会累积)
- 过早优化(先让测试通过再说)
9. 测试代码维护策略
- 每周花10分钟删除过时测试
- 测试失败时优先修复而非跳过
- 建立团队测试规范(比如禁用Thread.sleep)
我的团队checklist:
- [ ] 测试方法名表达意图
- [ ] 每个测试<15行代码
- [ ] 没有魔法数字
- [ ] 异常case有对应测试
10. 现代测试工具链
10.1 代码生成工具
- Evosuite(Java自动生成测试)
- Testim(基于AI的测试生成)
10.2 突变测试
使用PITest检测测试有效性:
xml复制<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.7.3</version>
</plugin>
10.3 可视化工具
- Jest --coverage生成的HTML报告
- SonarQube测试质量看板
记住:好的单元测试应该像优秀的副驾驶——平时安静不打扰,关键时刻能救命。当你发现自己在调试上花的时间越来越少,就说明你的测试开始产生复利了。
