单元测试核心思想:FIRST 原则
测试是保障代码质量的最后一道防线。
很多人写代码很认真,但测试就随便写两个 assert 完事。等线上出了 bug,才发现那个没测到的边界条件才是罪魁祸首。
测试和代码一样,需要认真对待。
什么是单元测试
单元测试是对软件的最小可测试单元进行验证。在 Java 里,通常是对类的方法进行测试。
单元测试 vs 集成测试
| 对比项 | 单元测试 | 集成测试 |
|---|---|---|
| 测试范围 | 单个类/方法 | 多个组件协作 |
| 依赖 | Mock 外部依赖 | 使用真实依赖 |
| 执行速度 | 毫秒级 | 秒到分钟 |
| 稳定性 | 高 | 中(依赖环境) |
| 定位问题 | 精确 | 模糊 |
FIRST 原则
F:Fast(快速)
慢的测试会被开发忽略:
java
// ❌ BAD:包含耗时操作的测试
@Test
void testBad() {
Thread.sleep(5000); // 模拟网络延迟 5 秒
// 测试代码
}
// ✅ GOOD:用 Mock 替代真实调用
@Test
void testGood() {
when(httpClient.get("http://example.com")).thenReturn(mockResponse);
// 测试代码,毫秒级完成
}I:Independent(独立)
测试之间不能有依赖,一个挂了不影响另一个:
java
// ❌ BAD:测试之间共享状态
class BadTest {
private final List<String> list = new ArrayList<>();
@Test
void testAdd() {
list.add("item"); // 修改了共享状态
assertEquals(1, list.size());
}
@Test
void testContains() {
// 依赖 testAdd 先执行,不可靠
assertTrue(list.contains("item"));
}
}
// ✅ GOOD:每个测试独立创建自己的数据
class GoodTest {
@Test
void testAdd() {
List<String> list = new ArrayList<>(); // 测试自己持有
list.add("item");
assertEquals(1, list.size());
}
@Test
void testContains() {
List<String> list = new ArrayList<>(); // 独立创建
assertFalse(list.contains("item")); // 不依赖其他测试
}
}R:Repeatable(可重复)
测试应该每次都跑出相同的结果:
java
// ❌ BAD:依赖外部状态
@Test
void testBad() {
File file = new File("/tmp/test.txt"); // 文件可能不存在
}
// ✅ GOOD:创建临时文件,用完即删
@Test
void testGood() {
Path tempFile = Files.createTempFile("test", ".txt");
try {
// 测试代码
} finally {
Files.deleteIfExists(tempFile);
}
}S:Self-Validating(自验证)
测试能自己判断通过还是失败:
java
// ❌ BAD:需要人工判断
@Test
void testBad() {
System.out.println("检查输出是否正确");
// 人来判断?自动化测试的意义何在?
}
// ✅ GOOD:用断言
@Test
void testGood() {
assertEquals(4, 2 + 2); // 自动化判断
}T:Timely(及时)
TDD 主张测试先行,但即使不做 TDD,也要在写完代码后立刻补测试。
测试结构:Given-When-Then
这是最流行的测试结构,把测试分成三个清晰的阶段:
java
@Test
@DisplayName("用户注册 - 成功")
void register_Success() {
// Given:准备测试数据
UserRequest request = new UserRequest();
request.setUsername("testuser");
request.setPassword("password123");
// When:执行被测方法
UserResponse response = userService.register(request);
// Then:验证结果
assertThat(response)
.isNotNull()
.returns("testuser", UserResponse::getUsername)
.returns(notNullValue(), UserResponse::getId);
}常见测试场景
测试正常流程
java
@Test
void testCalculateTotalPrice() {
Order order = new Order();
order.addItem(new OrderItem("商品1", 100.0, 2));
order.addItem(new OrderItem("商品2", 50.0, 1));
double total = order.calculateTotalPrice();
assertEquals(250.0, total, 0.01); // 第三个参数是 delta,允许浮点误差
}测试边界条件
java
@Test
void testDivideByZero() {
assertThrows(ArithmeticException.class, () -> {
calculator.divide(10, 0);
});
}
@Test
void testNullInput() {
assertThrows(IllegalArgumentException.class, () -> {
service.process(null);
});
}测试异常消息
java
@Test
void testExceptionMessage() {
Exception exception = assertThrows(RuntimeException.class, () -> {
throw new RuntimeException("用户不存在: 12345");
});
assertThat(exception.getMessage())
.contains("12345");
}总结
测试的核心原则:
- 快速:毫秒级完成,不拖慢开发节奏
- 独立:不依赖其他测试的执行顺序
- 可重复:相同输入永远得到相同结果
- 自验证:用断言,不用人工判断
- 及时:代码写完就补测试
不写测试的代码,就像没有安全带的赛车——快是快,但一旦出事就是大事。
