第4章 断言机制:assertEquals、assertTrue、assertThrows、assertAll 的使用与区别
断言,说白了就是测试里的“交警”——拦住不该通过的情况。我工作头两年最怕的就是这种场景:跑了一百个测试全绿,结果线上还是崩了。后来查下来,发现是断言写得有问题,根本没抓到真正的 bug。嗯,从那以后我就把断言机制当成了单元测试的第一道防线。
JUnit 4 时代我们常用 Assert 类那一套,从 JUnit 5 开始变成了 Assertions 类。核心方法也就四五个,但用不好很容易写出“假绿”测试。今天咱们把这四个断言方法掰开揉碎了聊一聊。
assertEquals:值的精准匹配
这是最常用的断言。我个人的习惯是:能用 assertEquals 的地方绝不用 assertTrue 加条件比较。举个简单的例子,你要验证一个计算结果是 42,直接 assertEquals(42, result) 就够了。失败时会清楚打出期望值和实际值,省得你再去猜。
你想想看,如果写成 assertTrue(result == 42),虽然也能跑,但失败信息只有“expected true but was false”,你根本不知道 result 到底是多少。这就是我常说的“写测试也要有同理心”——明天接手的人(包括下个月的你自己)不希望看到一个问号的测试日志。
@Test
void testAdd() {
Calculator calc = new Calculator();
int sum = calc.add(10, 20);
assertEquals(30, sum, "10 + 20 应该等于 30"); // 加一句message
}
assertEquals 对数组比较不是逐个元素比。要比较数组内容,用 assertArrayEquals。我以前在这里吃过亏——两个内容一模一样的整型数组,assertEquals 愣是红了,我还以为是算法写错了。
assertTrue 和 assertFalse:布尔条件的守卫
这两个方法适用于验证条件成立或不成立。但请注意,assertTrue 接受的可以是一个 BooleanSupplier,也就是延迟计算。我建议:如果条件本身很重(比如要查数据库),尽量用 lambda 形式,避免每次构造测试都白算一遍。
@Test
void testCondition() {
List<String> list = new ArrayList<>();
list.add("Hello");
// 用BooleanSupplier延迟计算
assertTrue(() -> list.size() > 0, "列表不应为空");
}
在项目中我遇到过这样的场景:有人用 assertTrue(list.isEmpty()) 来验证列表为空,其实 assertTrue 完全没问题,但可读性不如 assertFalse(list.size() > 0)。说白了,布尔断言更多是给“自定义条件”准备的,而不是替代 equals。
assertThrows:优雅地捕获异常
我最早写测试时验证异常是用 try-catch 手工抓,然后在 catch 里 assert 一下。JUnit 5 的 assertThrows 把这套全简化了。它不仅验证异常类型,还会返回异常对象,你可以进一步检查异常消息。
我曾经踩过一个坑:子类异常用 assertThrows(Exception.class) 也能通过——因为父类异常会匹配子类。如果你只想精确匹配某个特定异常,不要在泛型里写得太宽。
@Test
void testDivideByZero() {
Calculator calc = new Calculator();
ArithmeticException ex = assertThrows(
ArithmeticException.class,
() -> calc.divide(1, 0),
"除零应抛异常"
);
assertEquals("/ by zero", ex.getMessage());
}
assertThrows 会直接让测试失败。所以它内部其实也做了 try-catch 和失败统计,只不过对我们透明了。
assertAll:一次性收集所有失败
这个堪称“对象验证神器”。假设你要验证一个 User 对象的三个字段,按照传统写法,第一个断言失败后面的就不跑了——你改了名字再跑,发现邮箱也错了,来回折腾好几轮。而 assertAll 会把所有断言都执行一遍,然后一次性告诉你哪几个失败了。
@Test
void testUser() {
User user = new User("张三", "zhangsan@example.com", 25);
assertAll("用户信息验证",
() -> assertEquals("张三", user.getName()),
() -> assertTrue(user.getEmail().contains("@")),
() -> assertTrue(user.getAge() > 0)
);
}
我个人习惯:只要是对一个对象的多个属性做断言,一律用 assertAll。你在重构的时候会感激这个做法——改了一个字段,其他字段的断言依然在运行,不会因为中途失败而停下。
四个断言的核心区别
| 断言方法 | 主要用途 | 参数特点 | 失败行为 | 我推荐使用场景 |
|---|---|---|---|---|
| assertEquals | 比较两个值是否相等 | 期望值、实际值、可选message | 立即失败,显示期望/实际 | 返回值验证、对象状态比较 |
| assertTrue/assertFalse | 验证布尔条件 | 布尔值或BooleanSupplier、可选message | 立即失败,显示“期望true但得到false” | 自定义条件、集合状态、lambda表达式 |
| assertThrows | 验证异常被抛出 | 异常类型、Executable、可选message | 立即失败,若无异常或异常类型不匹配 | 边界值测试、非法参数校验、错误处理验证 |
| assertAll | 分组执行多个断言,收集所有失败 | 描述、多个Executable | 所有断言执行完毕后,汇总所有失败 | 多字段对象验证、DAU测试、集成测试 |
实践中的选择逻辑
你可能会问:“那我到底该用哪个?”其实很简单:
- 要比较两个值 → assertEquals
- 要检查一个条件是真是假 → assertTrue/assertFalse
- 要确认代码一定会抛指定异常 → assertThrows
- 要一下子验证对象的多个属性 → assertAll
如果还有更复杂的需求(比如自定义一个符合你业务逻辑的断言),JUnit 5 也支持你写自己的 Extension,但一般人用不上。至少在写参数化测试和边界值案例时,上面四个方法已经能覆盖 95% 的场景。
一句话总结: 选择断言不是看哪个更酷,而是看哪个能让测试失败信息最易读、最精确。能用 equals 就别用布尔,能用 assertAll 就别分开写多个断言。