断言详解: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));

这样哪怕没有断点,看一眼日志就知道大概怎么回事。

知识一览

下面这张图把本章所有断言的用途、关键点和注意事项串了起来,方便你快速回顾。

断言类型速查图 值断言 assertEquals / assertNotEquals • 浮点数必须给 delta • 自定义消息懒加载 布尔断言 assertTrue / assertFalse • 可塞复杂条件表达式 • 消息最好写“期望什么” 异常断言 assertThrows • 指定最具体的异常类 • 可获取异常对象验证消息 分组断言 assertAll • 一次执行多个断言 • 不因一个失败而中断 超时断言 assertTimeout / Preemptively • 优先用 assertTimeout • CI 环境留余量 自定义断言消息 所有断言都支持自定义消息,建议使用 Supplier<String> 实现懒加载 在回归测试中,好的消息能帮你省下 50% 的排错时间

本章要点:断言不是越多越好,而是越精准越好。学会组合使用 assertAll 和自定义消息,你的回归测试才能又快又稳。