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 获取。