单元测试概念:定义、目的、FIRST原则、可测性设计

单元测试,说白了就是验证我们写的那一小段代码逻辑对不对。很多人觉得这是可有可无的环节,但等你真正遇到线上事故,可能就不会这么想了。我记得刚入行时,觉得单元测试是浪费时间,后来有一次重构一个工具类,改了一个方法,结果导致好几个功能都崩了。嗯,从那以后我再也不敢把测试当摆设了。

什么是单元测试?

单元测试是对软件中最小可测试单元的验证工作。通常是一个函数、一个方法、或者一个类。它不依赖数据库、不依赖网络、不依赖其他服务。说白了就是把你写的代码,在隔离状态下跑一遍。

核心要点:单元测试 ≠ 集成测试。单元测试只管自己的逻辑,不关心外部依赖。如果你测试一个方法时还要启动数据库,那这就不是单元测试了。

为什么需要单元测试?

我个人总结下来,有这几个好处:

  • 快速反馈:写完代码立马就能知道有没有问题
  • 便于重构:重构完跑一遍测试,心里踏实
  • 活的文档:新同事看测试用例,就知道方法该怎么用
  • 回归保障:改了这个模块,其他模块不会受影响

你想想看,如果团队里没有人写单元测试,每次上线都靠手动点点,那是不是很悬?我在上家公司就遇到过,一个配置项改错了,全线上报错才发现,折腾了两小时。如果有单元测试,几秒钟就能发现问题。

FIRST原则

好的单元测试应该遵循 FIRST 原则。我习惯在写测试用例前过一遍这五个词,避免写出无效的测试。

缩写 含义 说明
F Fast(快速) 跑一批测试应该在秒级完成。如果跑一次要几分钟,没人愿意执行
I Isolated(隔离) 测试之间不能互相影响。顺序不能影响结果
R Repeatable(可重复) 每次运行结果都一样。不要依赖随机数或时间戳
S Self-validating(自验证) 测试自己就能判断通过还是失败,不需要人工看日志
T Timely(及时) 最好在写功能代码之前写测试,最晚也要同步写

个人经验:我之前看到有人把测试用例写成了脚本——跑一次要三分钟,而且依赖数据库里的数据。这样的测试基本等于没用。因为没人愿意等,也没人敢信任结果。

可测性设计

想让单元测试好写,代码设计就要配合。我见过太多代码,一写测试就头疼——依赖太多了,构造对象要传二十个参数。这种代码,说白了就是设计有问题。

可测性设计的关键点:

  1. 依赖注入:需要的对象通过参数传进来,而不是在方法内部 new
  2. 接口抽象:依赖外部系统的地方,用接口隔离开来
  3. 单一职责:一个方法只做一件事,不要又算数据又发邮件

举个例子说明:

// 不好的设计 — 测试起来会很痛苦
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);

注意:我曾经遇到过一个项目,所有方法都是静态方法,全局状态满天飞。当时想写单元测试,根本无从下手。后来花了三天时间重构,才把测试补上。如果你现在在写新代码,尽量避开静态变量、全局配置这些坑。不然以后测试会很难受。

单元测试核心知识结构 单元测试概念 什么是单元测试 为什么需要它 FIRST 原则 可测性设计 质量底线 隔离、快速、可重复 不依赖外部系统 依赖注入、接口抽象 单一职责原则 保障重构、回归、代码文档

上面这张图是我自己对单元测试知识体系的梳理。你看,核心就是定义、目的、原则、设计方法这四块。每一块都相互关联,缺一不可。缺少了设计这块,写测试会很难受;忽略了原则,测试写了也白写。我个人建议在实际项目中,先搞清楚这几块再动手。

常见误区

  • 测试覆盖率越高越好:其实 80% 的核心逻辑覆盖就够了,别为凑数写无意义测试
  • 必须所有依赖都 mock:工具类或者简单数据对象,直接用真实对象更简单
  • 先写功能再写测试:这个顺序容易导致代码不可测,最后测试也不好写
  • 测试本身也要维护:如果改一次功能要改十个测试,说明测试设计也出问题了

一个小技巧:写测试之前,先想想这个测试有没有用。如果这个测试跑失败了,我能知道具体哪里出了问题吗?如果答案是肯定的,那就是个好测试。