断言详解:assertEquals、assertTrue、assertThrows、assertAll、assertTimeout 等
测试用例到底写得对不对?说白了,全靠断言来撑腰。断言就是测试的眼睛,你写再多逻辑,如果断言没用到点子上,那测试结果就是睁眼瞎。我个人习惯把断言当成「测试的裁判」——裁判没吹哨,比赛不算完。
这章咱们把 JUnit5 里最常用的几个断言挨个捋一遍。顺便聊聊自定义消息,这个东西你一开始可能觉得啰嗦,但出问题的时候你就知道它多香了。
assertEquals 与 assertNotEquals
这两个是最基础的「值相等」和「值不相等」断言。注意我说的是「值」,不是「引用」。对基本类型,比的是值;对对象,比的是 equals() 方法。
关键点:浮点数断言一定要指定 delta。我在项目中遇到过两次线上 bug,都是因为浮点计算精度不一致,结果 assertEquals(0.1 + 0.2, 0.3) 竟然不通过……
@Test
void testEquals() {
assertEquals(10, 10);
assertEquals("Hello", "Hello", "字符串应该相等"); // 自定义消息
assertEquals(0.1 + 0.2, 0.3, 0.0001); // 浮点数必须给delta
}
@Test
void testNotEquals() {
assertNotEquals(1, 2);
}
自定义消息放哪?第三个参数。它只在断言失败时才会计算,不浪费性能。我曾经见过有人写 assertEquals(a, b, "a=" + a + ", b=" + b),这样做每次都会拼接字符串,性能有损耗。推荐用 Supplier<String> 形式,懒加载。
assertEquals(actual, expected, () -> "值不匹配,expected: " + expected + ", actual: " + actual);
assertTrue 与 assertFalse
布尔条件测试。很多人只把它当成 if 的替代品,其实你想想看,它可以塞复杂的条件表达式。我习惯把多个条件的组合也放进去,这样比拆成多个 assertEquals 更直观。
技巧:自定义消息在布尔断言里格外重要。否则失败日志只告诉你「expected: true but was: false」,你根本不知道哪个条件没满足。
@Test
void testBoolean() {
List<String> list = Arrays.asList("a", "b", "c");
assertTrue(list.contains("b"), "列表应该包含b");
assertFalse(list.isEmpty(), "列表不应该为空");
}
assertThrows
异常断言是回归测试里很常用的一项。你写了一个方法,期望它在参数非法时抛出 IllegalArgumentException,那就用 assertThrows 验证。返回值是异常对象,你还可以进一步检查异常的 message。
避坑指南:我曾经写错过异常类型匹配。比如方法实际抛 RuntimeException,但我不小心写了 Exception.class,结果断言通过了——因为 Exception 是父类!切记:写最具体的异常类,不要偷懒。
@Test
void testThrow() {
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> Integer.parseInt("abc"),
"应该抛出IllegalArgumentException");
assertTrue(ex.getMessage().contains("For input string")); // 验证异常消息
}
assertAll
这个是我做回归测试时最爱用的断言之一。它可以把多个断言「打包」执行,哪怕其中一个失败了,其他的也会继续跑完,最后把所有失败信息一次性告诉你。而不是像普通断言那样,碰到第一个失败就停住。
@Test
void testMultiple() {
User user = createUser();
assertAll("用户属性校验",
() -> assertEquals("John", user.getName()),
() -> assertTrue(user.getAge() > 0),
() -> assertNotNull(user.getEmail())
);
}
你想想看,如果没有 assertAll,你第一次失败后修复了一个,第二次跑又发现第二个失败,反复跑浪费时间。用 assertAll 一次看清所有问题,在回归测试里尤其高效。
assertTimeout 与 assertTimeoutPreemptively
超时断言用于检测代码是否在规定时间内执行完。注意两个版本的区别:assertTimeout 会等到任务执行完再判断是否超时;assertTimeoutPreemptively 会在超时后强行中断线程(但会带来副作用,比如数据库连接可能没关)。
我个人建议,在单元测试里优先用 assertTimeout,除非你知道线程安全没问题。我有一次在 CI 里用 assertTimeoutPreemptively 测试一个文件写入操作,结果超时中断后文件状态不一致,排查了大半天。
@Test
void testTimeout() {
assertTimeout(Duration.ofMillis(100), () -> {
Thread.sleep(50); // 模拟正常耗时
}, "执行不应超过100ms");
}
注意:CI 环境可能比你的机器慢很多,超时阈值不要设太紧。我一般留 2~3 倍余量,避免误报。
自定义断言消息:为什么值得写
很多初学者觉得自定义消息「又不是不能跑」,懒得写。但你在回归测试失败时,面对一堆 expected: true but was: false,大脑要花额外时间去猜「哪个 true 没满足」。如果每个断言都加了类似 "用户邮箱不能为空" 的消息,定位问题速度能快 3 倍。
我还有一个习惯:把关键变量的值也拼进消息里。比如:
assertEquals(200, response.statusCode(),
() -> "登录接口返回状态码不符,实际: " + response.statusCode() + ",响应体: " + response.body().substring(0, 200));
这样哪怕没有断点,看一眼日志就知道大概怎么回事。
知识一览
下面这张图把本章所有断言的用途、关键点和注意事项串了起来,方便你快速回顾。
本章要点:断言不是越多越好,而是越精准越好。学会组合使用 assertAll 和自定义消息,你的回归测试才能又快又稳。