Skip to content

断言

本文介绍断言(Assertion)用法。

1. 概述

在编程和软件开发中,断言(Assertion) 是一种用于在代码中检查“假设是否成立”的机制。在JUnit中,断言允许开发者检查特定条件是否为真,如果不为真,则会报告测试失败。

断言通常表现为一个带有布尔表达式的函数或关键字,例如 assert(condition)。

  • 如果条件为真(True):什么都不会发生,程序继续正常向下执行。
  • 如果条件为假(False):说明程序出现了意料之外的错误(Bug),断言会被触发,通常会抛出一个特定的错误(如 AssertionError),并立即终止程序的运行。

2. Java中的断言

在Java语法层面上,也提供了断言机制,使用关键词assert,例如:

java
public static void main(String[] args) {
    int length = 1;
    assert length < 0;
}

在 Java 中,断言默认是关闭的,必须使用 -ea (enableassertions) 启动参数才会生效。所以直接运行上面的程序,没有任何反应。

开启断言后再运行,会发现程序抛出了异常:Exception in thread "main" java.lang.AssertionError

3. JUnit中的断言

在JUnit中,也提供了很多断言方法,这些都是在org.junit.jupiter.api.Assertions类中的静态方法。

JUnit 允许使用 Supplier<String> (Lambda) 作为错误信息,只有在断言真正失败时,才会去计算和拼接字符串,提升测试性能。

以下是 JUnit Jupiter 中最核心、最常用的断言方法分类介绍:

3.1 基础条件断言

这些是最常用的断言,用于比较基本类型、对象或布尔状态。

  • assertEquals(expected, actual):判断期望值和实际值是否相等。
  • assertNotEquals(unexpected, actual):判断两个值是否不相等。
  • assertTrue(condition) / assertFalse(condition):判断条件是否为 true 或 false。
  • assertNull(actual) / assertNotNull(actual):判断对象是否为 null。
  • assertSame(expected, actual) / assertNotSame(unexpected, actual):判断两个对象引用是否指向内存中的同一个对象(相当于 ==)。

例如:

java
@Test
void test() {
    Integer result = Calculator.add(1, 1);

    // 基础用法:期望值是 2,实际值是 result,最后一个参数是失败时的提示信息
    assertEquals(2, result, "1 + 1 应该等于 2");
    // 高级用法:使用 Lambda 延迟生成复杂的错误信息
    assertTrue(result > 0, () -> "计算结果必须大于 0,但实际是: " + result);
}

3.2 数组与集合断言

用于对比数组、列表等集合类型是否一致:

  • assertArrayEquals(expectedArray, actualArray):对比两个数组的内容、顺序和长度是否完全一致。
  • assertIterableEquals(expectedIterable, actualIterable):对比两个实现了 Iterable 接口的集合(如 List)。不仅要求元素相等,还要求元素的迭代顺序严格一致
  • assertLinesMatch(expectedLines, actualLines):专门用于对比字符串列表(List<String>)。它不仅支持普通的精确匹配,还支持正则表达式匹配(非常适合比对日志输出)。

例如:

java
@Test
void testAssertArray() {
    int[] array1 = {1, 2, 3};
    int[] array2 = {1, 2, 3};
    assertArrayEquals(array1, array2);
}
java
@Test
void testAssertIterable(){
    List<Integer> list1 = List.of(1, 2, 3);
    List<Integer> list2 = List.of(1, 2, 3);
    assertIterableEquals(list1, list2);
}
java
@Test
void testAssertLines(){
    List<String> list1 = List.of("\\d", "2", "3");
    List<String> list2 = List.of("1", "2", "3");

    assertLinesMatch(list1, list2);
}

3.3 异常断言

  • assertThrows(expectedType, executable):断言代码块(Lambda)一定会抛出指定类型(或指定类型的子类)的异常。
  • assertThrowsExactly(expectedType, executable):断言代码块(Lambda)一定会抛出指定类型的异常。如果代码块抛出指定类型子类异常,该断言并不会视为失败。
  • assertDoesNotThrow(executable):断言代码块绝对不会抛出任何异常。

assertThrows 会返回捕获到的异常对象,可以进一步对异常的 Message 或错误码进行断言:

java
@Test
void testAssertThrows(){
    // 断言代码块一定会抛出 ArithmeticException
    ArithmeticException exception = assertThrows(ArithmeticException.class, () -> {
        int a = 1 / 0; // 触发除以零异常
    });

    // 进一步断言异常信息是否符合预期
    assertEquals("/ by zero", exception.getMessage());
}

3.4 组合断言

通常情况下,如果一个测试方法里有 5 个断言,第 1 个失败了,后面的 4 个就不会执行了(Fail-fast)。但有时我们希望执行完所有的断言,然后一次性报告所有失败的地方。

  • assertAll(heading, executables...):接收多个 Executable (Lambda),并行/顺序执行。即使前面的失败了,后面的依然会执行。
java
@Test
void testAssertAll() {
    assertAll("测试assertAll",
            () -> assertEquals(2, 1),
            () -> assertEquals(3, 1),
            () -> assertEquals(1, 1)
    );
}

3.5 超时断言

用于测试代码的性能或防止死循环。

  • assertTimeout(duration, executable):断言代码块在规定时间内执行完毕。如果超时,它会等待代码执行完毕后,再报告超时失败(能告诉你具体超了多少时间)。
  • assertTimeoutPreemptively(duration, executable):抢占式超时断言。如果时间到了代码还没跑完,会立刻在另外的线程中强行终止代码的执行并报错。
java
@Test
void testAssertTimeout(){
    // 断言这个方法能在 1 秒内跑完
    assertTimeoutPreemptively(Duration.ofSeconds(1), () -> {
        // 模拟一段耗时操作
        Thread.sleep(1800);
    });
}

4. 异常处理

默认情况下,如果异常从测试方法、生命周期方法或扩展方法抛出,并且该异常没有被捕获,那么JUnit会将该测试视为失败:

java
@Test
void testException(){
    int i = 1/0;
}

实际上,JUnit中的断言机制,就是通过抛出AssertError异常来实现的。

5. 假设

假设(Assumption)用于验证当前**测试运行的先决条件(前置环境)**是否满足。它不是用来测试代码的,而是用来判断“当前这个环境适不适合运行这个测试”。

如果假设失败,JUnit 会抛出 TestAbortedException异常。这个测试不会失败,而是会被中断并标记为 SKIPPED(跳过 / 忽略)。这意味着代码没问题,只是当前环境不具备运行此测试的条件。

例如,某个测试只能在 Windows 操作系统上运行;或者某个测试需要特定的环境变量、数据库连接等,如果没有连接上,直接跳过即可,没必要报红(报错)。

假设的常用方法如下:

  • assumeTrue(condition, message):假设给定的条件为true,如果为false,则抛出TestAbortedException异常,消息为message
  • assumeFalse(condition, message):假设给定的条件为false,如果为true,则抛出TestAbortedException异常,消息为message
  • assumingThat(boolean assumption, Executable executable):假设为true时,执行executable程序块;
  • abort(message):直接标记测试中断;

例如:

java
@Test
void testWindowsFilePath() {
    // 假设:这个测试必须在 Windows 系统上运行
    // 如果当前是 MacOS 系统,这行代码会中止测试,该用例被标记为“Skipped”而不是“Failed”
    assumeTrue(System.getProperty("os.name").contains("Windows"), "中止:不是 Windows 系统");

    // 只有上面假设成立,才会执行下面的测试逻辑和断言
    String path = "C:\\test\\";
    assertEquals("C:\\test\\", path);
}

结果如下:

image-20260220150736367

6. AssertJ

6.1 概述

虽然JUnit中提供了很多断言方法,但是在一些常见下,使用第三方断言库更有必要,第三方断言库包括 AssertJ, Hamcrest, Truth等,此处介绍AssertJ断言库。

AssertJ 致力于让测试代码读起来像写英语句子一样自然,并且让错误信息更加有用。在实际应用中,一般都使用AssertJ作为断言库。

AssertJ由以下模块组成:

  • core模块:提供对JDK类型(String, Iterable, Stream, Path, File, Map, …)的断言;
  • Guava模块:提供对Guava类型(Multimap, Optional, …)的断言;
  • Joda Time模块:提供对Joda Time类型(DateTime, LocalDateTime)的断言;
  • Neo4J模块:提供对Neo4J类型(Path, Node, Relationship, …)的断言;
  • DB模块:提供对关系数据库类型 (Table, Row, Column, …)的断言;
  • Swing模块:提供对Swing界面的断言;

本次只介绍core模块,首先引入依赖:

xml
<!-- Source: https://mvnrepository.com/artifact/org.assertj/assertj-core -->
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.27.7</version>
    <scope>test</scope>
</dependency>

下面介绍AssertJ的优势。

6.2 流式API

  • JUnit (传统写法):需要记忆参数顺序(哪个是期望值,哪个是实际值?)。
  • AssertJ:符合人类思维的“主谓宾”结构,从左到右阅读。

例如,要验证一个字符串 name 是 "Alice":

java
// JUnit 5: 参数顺序容易记混 (expected, actual)
assertEquals("Alice", name); 

// AssertJ: 就像在读英语句子,逻辑极度清晰
assertThat(name).isEqualTo("Alice");

6.3 处理集合(List/Map)强大

例如:

java
List<String> users = Arrays.asList("Alice", "Bob", "Charlie");

// --- JUnit 5 写法 (比较啰嗦) ---
assertEquals(3, users.size());
assertTrue(users.contains("Alice"));
assertTrue(users.contains("Bob"));
assertFalse(users.contains("David"));

// --- AssertJ 写法 (一行搞定,逻辑连贯) ---
assertThat(users)
    .hasSize(3)                  // 长度是3
    .contains("Alice", "Bob")    // 包含这两个
    .doesNotContain("David")     // 不包含这个
    .startsWith("Alice")         // 第一个是 Alice
    .doesNotHaveDuplicates();    // 没有重复元素

6.4 高级特性

AssertJ 能处理很多复杂的业务场景,避免编写大量的样板代码。

  • 提取对象属性(Extracting):验证 List 中对象的某个字段。

    java
    // 验证所有用户的名字是 "Alice" 和 "Bob"
    assertThat(userList).extracting("name").contains("Alice", "Bob");
  • 日期与时间

    java
    // 验证日期是否在某个范围内
    assertThat(actualDate).isBetween("2023-01-01", "2023-12-31");
  • 异常断言

    java
    assertThatThrownBy(() -> { throw new Exception("boom!"); })
        .isInstanceOf(Exception.class)
        .hasMessageContaining("boom");

参考资料

[1] JUnit中的断言:https://docs.junit.org/6.0.3/writing-tests/assertions.html

[2] 异常处理:https://docs.junit.org/6.0.3/writing-tests/exception-handling.html

[3] AssertJ:https://assertj.github.io/doc/