3、断言机制:assertEquals、assertTrue、assertFalse、assertNull、assertThrows 等常用断言方法

断言,说白了就是「拍板定案」的那一下。

你写了一大堆测试代码,跑了一堆逻辑,最后怎么知道结果对不对?靠的就是断言。我个人习惯把断言看作是测试的「裁判员」——它不参与比赛,但吹哨的那一刻,决定了你的代码是赢是输。

3.1 assertEquals:最常用的「等不等」

这是你接触最多的断言方法。它的作用很简单:判断两个值是否相等。

@Test
void testAdd() {
    Calculator calc = new Calculator();
    int result = calc.add(2, 3);
    assertEquals(5, result);
}

这里有个坑,我刚开始写测试时踩过好几次。assertEquals 的第一个参数是「期望值」,第二个才是「实际值」。顺序搞反了,测试照样能过,但报错信息会让人摸不着头脑。

⚠️ 注意: assertEquals 对基本类型和对象的行为不一样。基本类型比较值,对象比较的是 equals() 方法。如果你自定义了一个类,记得重写 equals(),否则 assertEquals 会拿内存地址去比,结果大概率是 false。

我曾经接手过一个老项目,里面有个 User 类没重写 equals(),结果 assertEquals 怎么跑都是红的。排查了半天,最后发现是这个问题。嗯,从那以后我写新类第一件事就是生成 equals() 和 hashCode()。

3.2 assertTrue 与 assertFalse:真假判断

这两个方法用来判断布尔表达式。说白了就是「是」还是「不是」。

@Test
void testIsAdult() {
    Person person = new Person(20);
    assertTrue(person.isAdult());
    
    Person child = new Person(15);
    assertFalse(child.isAdult());
}

我个人习惯把 assertTrue 用在条件判断比较复杂的场景。比如验证一个集合是否为空,或者某个状态是否激活。

💡 小技巧: assertTrue 可以加第二个参数,自定义失败时的提示信息。比如 assertTrue(user.isActive(), "用户应该处于激活状态")。这样测试挂了,你一眼就知道哪里出了问题。

你想想看,如果测试失败了只给你一个「expected true but was false」,你还要去翻代码找原因。加个提示信息,省了多少事。

3.3 assertNull 与 assertNotNull:空与非空

这两个方法专门用来判断对象是否为 null。为什么需要单独的方法?因为 null 在 Java 里太特殊了,它既不是 true 也不是 false,更不等于任何值。

@Test
void testFindUser() {
    UserService service = new UserService();
    
    User user = service.findById(1L);
    assertNotNull(user, "用户ID为1的记录应该存在");
    
    User notExist = service.findById(999L);
    assertNull(notExist, "不存在的用户应该返回null");
}

我记得有一次线上事故,就是因为一个方法在某些条件下返回了 null,但调用方没做判空处理,直接报了 NullPointerException。从那以后,我写测试时特别关注 null 的场景——不仅要测「有值」的情况,更要测「没值」的情况。

🔑 核心原则: 每个可能返回 null 的方法,都应该有对应的 assertNull 和 assertNotNull 测试用例。这是防御性编程在测试中的体现。

3.4 assertThrows:异常也是结果

这个方法是我最喜欢的。为什么?因为它让异常变成了可测试的「结果」,而不是测试的「意外」。

@Test
void testDivideByZero() {
    Calculator calc = new Calculator();
    
    assertThrows(ArithmeticException.class, () -> {
        calc.divide(10, 0);
    });
}

assertThrows 的第一个参数是期望的异常类型,第二个参数是一个 Lambda 表达式。如果这段代码抛出了指定类型的异常,测试通过;否则失败。

我曾经遇到过一个问题:一个方法在某些条件下会抛异常,但异常类型写错了。比如应该抛 IllegalArgumentException,结果抛了 RuntimeException。测试里用 assertThrows(IllegalArgumentException.class) 去测,死活不通过。后来发现是异常类型不匹配。嗯,这里要注意,异常类型必须精确匹配,子类不行。

⚠️ 注意: assertThrows 只检查异常类型,不检查异常消息。如果你需要验证异常消息的内容,可以用 assertThrows 的返回值:
@Test
void testDivideByZeroWithMessage() {
    Calculator calc = new Calculator();
    
    ArithmeticException exception = assertThrows(ArithmeticException.class, () -> {
        calc.divide(10, 0);
    });
    
    assertEquals("/ by zero", exception.getMessage());
}

3.5 其他常用断言方法

除了上面几个,JUnit 5 还提供了一些实用的断言方法,我简单列一下:

方法 用途 示例
assertSame 判断两个引用是否指向同一个对象 assertSame(expected, actual)
assertNotSame 判断两个引用是否指向不同对象 assertNotSame(expected, actual)
assertArrayEquals 判断两个数组是否相等 assertArrayEquals(expected, actual)
assertIterableEquals 判断两个可迭代对象是否相等 assertIterableEquals(expected, actual)
assertTimeout 判断代码是否在指定时间内执行完成 assertTimeout(Duration.ofSeconds(1), () -> { ... })
💡 个人建议: assertSame 和 assertEquals 的区别要搞清楚。assertSame 用的是 ==,比较的是内存地址;assertEquals 用的是 equals(),比较的是内容。我见过有人用 assertSame 去比较两个内容相同的字符串,结果测试挂了,因为两个字符串对象不是同一个引用。

3.6 断言的最佳实践

写了这么多年测试,我总结了几条关于断言的经验:

  1. 一个测试方法只测一个逻辑点。 别在一个 @Test 方法里写七八个断言,那样测试挂了你都不知道是哪个条件出了问题。
  2. 断言要写清楚失败信息。 尤其是团队协作的项目,别人看到你的测试失败了,能根据提示信息快速定位问题。
  3. 别滥用 assertTrue。 能用 assertEquals 就用 assertEquals,因为 assertEquals 的报错信息更明确——它会告诉你期望值和实际值分别是什么。
  4. 异常测试不能少。 很多开发只测正常流程,忽略了异常流程。但线上出问题,往往就是异常流程没覆盖到。

我记得有一次,一个同事写的测试里全是 assertTrue,连判断两个值相等都用 assertTrue(a == b)。结果测试挂了,报错信息只有「expected true but was false」,他花了半小时才找到是哪个值不对。从那以后,我要求团队里必须用最精确的断言方法。

好了,断言这块就讲到这里。说白了,断言就是测试的「眼睛」——你看不到代码执行的过程,但断言能告诉你结果对不对。用好断言,你的测试才算真正有了灵魂。