第4章 断言机制:assertEquals、assertTrue、assertThrows、assertAll 的使用与区别

断言,说白了就是测试里的“交警”——拦住不该通过的情况。我工作头两年最怕的就是这种场景:跑了一百个测试全绿,结果线上还是崩了。后来查下来,发现是断言写得有问题,根本没抓到真正的 bug。嗯,从那以后我就把断言机制当成了单元测试的第一道防线。

JUnit 4 时代我们常用 Assert 类那一套,从 JUnit 5 开始变成了 Assertions 类。核心方法也就四五个,但用不好很容易写出“假绿”测试。今天咱们把这四个断言方法掰开揉碎了聊一聊。

JUnit 5 断言机制核心逻辑 断言断言断言 🤖 assertEquals assertTrue assertThrows assertAll 比较两个值是否相等 • 支持基本类型、对象 • 重载方法带 message • 内部调用 equals() • 不支持数组直接比 • 建议加失败描述 ⚠ 注意 null vs 空串 验证布尔条件为真 • 参数为 BooleanSupplier • 延迟计算表达式 • 可配合 lambda • 不写空括号返回 • 反面用 assertFalse 💡 推荐用 isTrue() 验证异常被抛出 • 返回异常实例 • 泛型指定异常类型 • 可进一步断言 message • 只捕获指定异常 • 超类异常也可匹配 🔥 我踩过继承的坑 分组执行多个断言 • 所有断言都会执行 • 收集所有失败 • 失败信息一并显示 • 类似软断言 • 参数是 Executable… ⚡ 必用于对象校验 选择原则:按测试意图匹配最精确的断言

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 就别分开写多个断言。