1. 概述

本文将深入探讨 AssertJ 中专门用于异常断言的核心功能。作为 Java 开发者,优雅地验证异常行为是单元测试的必备技能,AssertJ 提供了多种简洁高效的解决方案。

2. 传统异常测试的痛点

在没有 AssertJ 的时代,我们通常这样测试异常:

try {
    // 可能抛出异常的代码
} catch (Exception e) {
    // 手动断言异常属性
}

⚠️ 这种方式存在明显缺陷:

  • 如果代码未抛出异常,测试会意外通过(假阳性)
  • 需要手动调用 fail() 方法才能正确处理未抛出异常的情况
  • 断言逻辑与异常捕获耦合,代码可读性差

3. AssertJ 异常断言实战

借助 Java 8 的 lambda 表达式,AssertJ 让异常测试变得异常优雅。以下是几种常用模式:

3.1 使用 assertThatThrownBy()

当需要验证列表越界异常时:

assertThatThrownBy(() -> {
    List<String> list = Arrays.asList("String one", "String two");
    list.get(2); // 必然抛出 IndexOutOfBoundsException
}).isInstanceOf(IndexOutOfBoundsException.class)
  .hasMessageContaining("Index: 2, Size: 2");

✅ 核心优势:

  • 将可能抛出异常的代码块作为 lambda 传递
  • 支持链式调用,断言逻辑清晰

可用的断言方法包括:

.hasMessage("Index: %s, Size: %s", 2, 2)  // 精确匹配消息
.hasMessageStartingWith("Index: 2")       // 匹配消息开头
.hasMessageContaining("2")                // 包含子串
.hasMessageEndingWith("Size: 2")          // 匹配消息结尾
.hasMessageMatching("Index: \\d+, Size: \\d+") // 正则匹配
.hasCauseInstanceOf(IOException.class)    // 验证异常原因
.hasStackTraceContaining("java.io.IOException") // 验证堆栈信息

3.2 使用 assertThatExceptionOfType

先指定异常类型,再验证行为:

assertThatExceptionOfType(IndexOutOfBoundsException.class)
  .isThrownBy(() -> {
      // 可能抛出异常的代码
}).hasMessageMatching("Index: \\d+, Size: \\d+");

这种写法在需要明确异常类型时更直观,IDE 通常能提供更好的代码提示。

3.3 针对常见异常的快捷方法

AssertJ 为常见异常类型提供了专用方法:

assertThatIOException().isThrownBy(() -> {
    // IO 操作代码
});

支持的快捷方法包括:

  • assertThatIllegalArgumentException()
  • assertThatIllegalStateException()
  • assertThatIOException()
  • assertThatNullPointerException()

3.4 分离异常捕获与断言

采用 BDD 风格的测试写法:

// when
Throwable thrown = catchThrowable(() -> {
    // 可能抛出异常的代码
});

// then
assertThat(thrown)
  .isInstanceOf(ArithmeticException.class)
  .hasMessageContaining("/ by zero");

这种写法特别适合复杂测试场景,能清晰区分"触发条件"和"验证逻辑"。

3.5 自定义异常字段断言

AssertJ 提供了多种方式验证自定义异常的字段。首先定义一个城市未找到异常:

public class CityNotFoundException extends RuntimeException {

    private String city;
    private String message;

    CityNotFoundException(String city, String message) {
        this.city = city;
        this.message = message;
    }

    // Getters
}

创建城市查询工具类:

public final class CityUtils {

    private static final List<String> CITIES = Arrays.asList("Tamassint", "London", "Madrid", "New york");

    public static String search(String searchedCity) {
        return CITIES.stream()
          .filter(searchedCity::equals)
          .findFirst()
          .orElseThrow(() -> new CityNotFoundException(searchedCity, "The specified city is not found"));
    }
}

方案一:使用 catchThrowableOfType()

@Test
public void whenUsingCatchThrowableOfType_thenAssertField() {
    String givenCity = "Paris";
    CityNotFoundException exception = catchThrowableOfType(() -> CityUtils.search(givenCity), CityNotFoundException.class);

    assertThat(exception.getCity()).isEqualTo(givenCity);
    assertThat(exception.getMessage()).isEqualTo("The specified city is not found");
}

方案二:使用 assertThatThrownBy() + extracting()

@Test
public void whenUsingAssertThatThrownBy_thenAssertField() {
    String givenCity = "Geneva";
    assertThatThrownBy(() -> CityUtils.search(givenCity))
      .isInstanceOf(CityNotFoundException.class)
      .extracting("city")  // 提取异常字段
      .isEqualTo(givenCity);
}

✅ 两种方案对比:

  • catchThrowableOfType():直接获取异常实例,适合多字段验证
  • extracting():链式调用更简洁,适合单字段快速验证

4. 总结

AssertJ 提供了多种优雅的异常断言方案:

  • assertThatThrownBy():通用型异常断言
  • assertThatExceptionOfType():类型优先的断言
  • 快捷方法:针对常见异常的专用语法糖
  • 分离式写法:BDD 风格的测试组织
  • 自定义异常验证:支持字段级断言

选择哪种方式取决于具体场景和团队偏好,但都能显著提升测试代码的可读性和维护性。完整示例代码可在 GitHub 获取。


原始标题:AssertJ Exception Assertions | Baeldung

« 上一篇: Smooks框架介绍