单元测试概念:定义、目的、FIRST原则、可测性设计
单元测试,说白了就是验证我们写的那一小段代码逻辑对不对。很多人觉得这是可有可无的环节,但等你真正遇到线上事故,可能就不会这么想了。我记得刚入行时,觉得单元测试是浪费时间,后来有一次重构一个工具类,改了一个方法,结果导致好几个功能都崩了。嗯,从那以后我再也不敢把测试当摆设了。
什么是单元测试?
单元测试是对软件中最小可测试单元的验证工作。通常是一个函数、一个方法、或者一个类。它不依赖数据库、不依赖网络、不依赖其他服务。说白了就是把你写的代码,在隔离状态下跑一遍。
核心要点:单元测试 ≠ 集成测试。单元测试只管自己的逻辑,不关心外部依赖。如果你测试一个方法时还要启动数据库,那这就不是单元测试了。
为什么需要单元测试?
我个人总结下来,有这几个好处:
- 快速反馈:写完代码立马就能知道有没有问题
- 便于重构:重构完跑一遍测试,心里踏实
- 活的文档:新同事看测试用例,就知道方法该怎么用
- 回归保障:改了这个模块,其他模块不会受影响
你想想看,如果团队里没有人写单元测试,每次上线都靠手动点点,那是不是很悬?我在上家公司就遇到过,一个配置项改错了,全线上报错才发现,折腾了两小时。如果有单元测试,几秒钟就能发现问题。
FIRST原则
好的单元测试应该遵循 FIRST 原则。我习惯在写测试用例前过一遍这五个词,避免写出无效的测试。
| 缩写 | 含义 | 说明 |
|---|---|---|
| F | Fast(快速) | 跑一批测试应该在秒级完成。如果跑一次要几分钟,没人愿意执行 |
| I | Isolated(隔离) | 测试之间不能互相影响。顺序不能影响结果 |
| R | Repeatable(可重复) | 每次运行结果都一样。不要依赖随机数或时间戳 |
| S | Self-validating(自验证) | 测试自己就能判断通过还是失败,不需要人工看日志 |
| T | Timely(及时) | 最好在写功能代码之前写测试,最晚也要同步写 |
个人经验:我之前看到有人把测试用例写成了脚本——跑一次要三分钟,而且依赖数据库里的数据。这样的测试基本等于没用。因为没人愿意等,也没人敢信任结果。
可测性设计
想让单元测试好写,代码设计就要配合。我见过太多代码,一写测试就头疼——依赖太多了,构造对象要传二十个参数。这种代码,说白了就是设计有问题。
可测性设计的关键点:
- 依赖注入:需要的对象通过参数传进来,而不是在方法内部 new
- 接口抽象:依赖外部系统的地方,用接口隔离开来
- 单一职责:一个方法只做一件事,不要又算数据又发邮件
举个例子说明:
// 不好的设计 — 测试起来会很痛苦
public class OrderService {
public void process(Order order) {
Database db = new Database();
EmailService.send(order.getEmail(), "订单已处理");
db.save(order);
}
}
// 好的设计 — 测试容易多了
public class OrderService {
private final Database db;
private final EmailService emailService;
public OrderService(Database db, EmailService emailService) {
this.db = db;
this.emailService = emailService;
}
public void process(Order order) {
emailService.send(order.getEmail(), "订单已处理");
db.save(order);
}
}
上面那个好的设计,测试时可以传入 mock 对象,不用真的连数据库。下面这段代码就是常见的测试写法:
// 使用 Mock 进行单元测试
Database mockDb = mock(Database.class);
EmailService mockEmail = mock(EmailService.class);
OrderService service = new OrderService(mockDb, mockEmail);
Order order = new Order("test@test.com", "商品A");
service.process(order);
verify(mockEmail).send("test@test.com", "订单已处理");
verify(mockDb).save(order);
注意:我曾经遇到过一个项目,所有方法都是静态方法,全局状态满天飞。当时想写单元测试,根本无从下手。后来花了三天时间重构,才把测试补上。如果你现在在写新代码,尽量避开静态变量、全局配置这些坑。不然以后测试会很难受。
上面这张图是我自己对单元测试知识体系的梳理。你看,核心就是定义、目的、原则、设计方法这四块。每一块都相互关联,缺一不可。缺少了设计这块,写测试会很难受;忽略了原则,测试写了也白写。我个人建议在实际项目中,先搞清楚这几块再动手。
常见误区
- 测试覆盖率越高越好:其实 80% 的核心逻辑覆盖就够了,别为凑数写无意义测试
- 必须所有依赖都 mock:工具类或者简单数据对象,直接用真实对象更简单
- 先写功能再写测试:这个顺序容易导致代码不可测,最后测试也不好写
- 测试本身也要维护:如果改一次功能要改十个测试,说明测试设计也出问题了
一个小技巧:写测试之前,先想想这个测试有没有用。如果这个测试跑失败了,我能知道具体哪里出了问题吗?如果答案是肯定的,那就是个好测试。