AssertJ 深度详解
AssertJ 是 Java 领域最强大的流式断言库,提供超过 2000 种断言方法,通过链式调用实现高度可读的测试验证。与传统的 JUnit 断言相比,AssertJ 提供更丰富的功能和更优雅的语法。
一、核心优势与架构
二、环境配置(Maven)
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.23.1</version>
<scope>test</scope>
</dependency>
三、基础断言实战
1. 简单对象断言
import static org.assertj.core.api.Assertions.*;
// 字符串断言
assertThat("Hello World")
.startsWith("Hello")
.endsWith("World")
.hasSize(11)
.containsIgnoringCase("hello");
// 数字断言
assertThat(42)
.isEven()
.isBetween(40, 50)
.isCloseTo(40, within(5));
// 布尔断言
assertThat(true).isTrue();
assertThat("").isEmpty();
2. 日期时间断言
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plusDays(1);
assertThat(today)
.isBefore(tomorrow)
.isAfter("2023-01-01")
.isInSameYearAs(LocalDate.of(2023, 12, 31))
.hasDayOfMonth(15); // 假设今天是15号
四、集合断言(核心优势)
1. 基础集合操作
List<String> fruits = Arrays.asList("apple", "banana", "orange");
assertThat(fruits)
.hasSize(3)
.contains("banana") // 包含元素
.containsOnly("orange", "banana", "apple") // 包含所有且仅包含
.doesNotContain("grape") // 不包含
.containsExactly("apple", "banana", "orange") // 精确顺序
.containsExactlyInAnyOrder("orange", "apple", "banana"); // 任意顺序
2. 高级集合过滤
List<Person> people = Arrays.asList(
new Person("Alice", 25),
new Person("Bob", 30),
new Person("Charlie", 35)
);
// 条件过滤
assertThat(people)
.filteredOn(person -> person.getAge() > 28)
.extracting(Person::getName)
.containsExactly("Bob", "Charlie");
// 属性过滤
assertThat(people)
.filteredOn("age", 25)
.extracting("name")
.containsOnly("Alice");
// 嵌套属性
assertThat(people)
.filteredOn("address.city", "New York")
.hasSize(2);
五、异常断言
1. 异常验证
// 验证异常类型和消息
assertThatThrownBy(() -> { throw new IOException("File not found"); })
.isInstanceOf(IOException.class)
.hasMessageContaining("not found")
.hasMessage("File not found");
// 更简洁的写法
assertThatExceptionOfType(IOException.class)
.isThrownBy(() -> { throw new IOException("Error"); })
.withMessage("Error");
2. 异常原因链
Exception rootCause = new IllegalArgumentException("Invalid input");
Exception cause = new RuntimeException("Processing error", rootCause);
Exception ex = new IOException("File error", cause);
assertThat(ex)
.hasRootCauseInstanceOf(IllegalArgumentException.class)
.hasStackTraceContaining("Processing error")
.hasMessageStartingWith("File");
六、对象与类断言
1. 对象属性验证
Person person = new Person("Alice", 30, new Address("Main St", "New York"));
assertThat(person)
.extracting("name", "age") // 提取多个属性
.containsExactly("Alice", 30);
assertThat(person)
.extracting(
Person::getName,
p -> p.getAddress().getCity()
)
.containsExactly("Alice", "New York");
// 递归字段验证
assertThat(person)
.usingRecursiveComparison()
.ignoringFields("id", "createdAt")
.isEqualTo(expectedPerson);
2. 类与接口
assertThat(Runnable.class)
.isInterface()
.hasMethods("run");
assertThat(ArrayList.class)
.isAssignableFrom(List.class)
.hasAnnotations(Deprecated.class);
七、文件与IO断言
File file = new File("test.txt");
assertThat(file)
.exists()
.isFile()
.canRead()
.hasName("test.txt")
.hasExtension("txt")
.hasContent("Hello AssertJ"); // 验证文件内容
Path path = Paths.get("src");
assertThat(path)
.isDirectory()
.hasParent(".")
.hasFileName("src");
八、自定义断言(扩展机制)
1. 创建自定义断言
// 业务对象
public class User {
private String username;
private boolean active;
// getters/setters
}
// 自定义断言类
public class UserAssert extends AbstractAssert<UserAssert, User> {
public UserAssert(User actual) {
super(actual, UserAssert.class);
}
public static UserAssert assertThat(User actual) {
return new UserAssert(actual);
}
// 自定义验证方法
public UserAssert hasValidUsername() {
isNotNull();
if (actual.getUsername() == null || actual.getUsername().length() < 5) {
failWithMessage("用户名必须至少5个字符");
}
return this;
}
public UserAssert isActive() {
isNotNull();
if (!actual.isActive()) {
failWithMessage("用户应处于激活状态");
}
return this;
}
}
2. 使用自定义断言
User user = userService.findUser("alice");
// 链式调用
UserAssert.assertThat(user)
.hasValidUsername()
.isActive()
.extracting(User::getEmail)
.isEqualTo("alice@example.com");
九、最佳实践与高级技巧
1. 软断言(收集所有错误)
SoftAssertions softly = new SoftAssertions();
softly.assertThat(user.getName()).isEqualTo("Alice");
softly.assertThat(user.getAge()).isBetween(18, 65);
softly.assertThat(user.getRoles()).contains("ADMIN");
// 最后验证所有断言
softly.assertAll(); // 如果任何失败,在此处报告所有错误
2. 断言描述增强
assertThat(account.getBalance())
.as("账户 %s 余额检查", account.getId())
.isPositive()
.describedAs("应大于最小余额") // 修改后续断言描述
.isGreaterThan(MIN_BALANCE);
3. 使用 Predicate 的自定义条件
// 自定义条件
Condition<String> validEmail = new Condition<>(
email -> email.matches("\\w+@\\w+\\.\\w{2,3}"),
"合法邮箱格式"
);
// 应用条件
assertThat("test@example.com").is(validEmail);
assertThat("invalid_email").isNot(validEmail);
十、AssertJ 与 JUnit5 集成
完整测试示例
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
class OrderServiceTest {
@Test
void placeOrder_ValidInput_CreatesOrder() {
// Arrange
OrderRequest request = new OrderRequest("A001", 2);
// Act
Order order = orderService.placeOrder(request);
// Assert
assertThat(order)
.isNotNull()
.extracting(Order::getId, Order::getStatus)
.containsExactly("ORD-12345", OrderStatus.CONFIRMED);
assertThat(order.getItems())
.hasSize(2)
.allMatch(item -> item.getPrice() > 0);
assertThat(order.getCreatedAt())
.isToday();
}
@Test
void placeOrder_InvalidProduct_ThrowsException() {
OrderRequest request = new OrderRequest("INVALID", 1);
assertThatThrownBy(() -> orderService.placeOrder(request))
.isInstanceOf(ProductNotFoundException.class)
.hasMessageContaining("INVALID");
}
}
十一、性能断言
assertThatCode(() -> {
// 性能敏感代码
heavyOperation();
})
.takesLessThan(100, TimeUnit.MILLISECONDS); // 执行时间小于100ms
十二、与 Mockito 协作
@ExtendWith(MockitoExtension.class)
class PaymentServiceTest {
@Mock PaymentGateway gateway;
@InjectMocks PaymentService service;
@Test
void processPayment_Success() {
// Stubbing
when(gateway.process(any())).thenReturn(true);
// Execute
PaymentResult result = service.processPayment(new Payment(100.0));
// Verify with AssertJ
assertThat(result)
.extracting(PaymentResult::isSuccess, PaymentResult::getTransactionId)
.contains(true, notNullValue());
}
}
十三、最佳实践总结
-
链式调用原则:
// 推荐:单断言链 assertThat(object) .isNotNull() .extracting(...) .satisfies(...); // 避免:多个独立断言 assertNotNull(object); assertEquals(value, object.getField());
-
测试结构优化:
// 使用提取和条件 assertThat(users) .filteredOn("active", true) .extracting("name", "email") .containsExactly( tuple("Alice", "alice@example.com"), tuple("Bob", "bob@example.com") );
-
错误信息增强:
assertThat(actual) .withFailMessage("期望值 %s 但实际是 %s", expected, actual) .isEqualTo(expected);
-
集合断言优先级:
十四、AssertJ 模块生态
模块 | 功能 | 依赖 |
---|---|---|
assertj-core | 核心断言库 | Java 8+ |
assertj-guava | Guava 集合支持 | Guava 依赖 |
assertj-db | 数据库断言 | 数据库连接 |
assertj-android | Android 扩展 | Android SDK |
assertj-swing | GUI 测试支持 | Swing 应用 |
assertj-joda-time | Joda-Time 支持 | Joda-Time 库 |
十五、与 JUnit 断言对比
场景 | JUnit | AssertJ |
---|---|---|
对象相等 | assertEquals(expected, actual) | assertThat(actual).isEqualTo(expected) |
集合包含 | assertTrue(collection.contains(x)) | assertThat(collection).contains(x) |
异常验证 | try/catch + fail() | assertThatThrownBy(...) |
多个属性验证 | 多个独立断言 | 链式调用 + extracting() |
自定义错误信息 | 参数传入 | .withFailMessage() |
软断言 | 不支持 | SoftAssertions |
十六、高级特性:递归比较
Person actual = new Person("Alice", 30, new Address("Main St", "New York"));
Person expected = new Person("Alice", 30, new Address("Main St", "New York"));
// 深度比较忽略特定字段
assertThat(actual)
.usingRecursiveComparison()
.ignoringFields("id", "createdAt")
.ignoringCollectionOrder()
.isEqualTo(expected);
// 比较特定字段
assertThat(actual)
.usingRecursiveComparison()
.comparingOnlyFields("name", "address.street")
.isEqualTo(expected);
AssertJ 核心价值:
- 可读性:自然语言般的链式调用
- 丰富性:超过 2000 种断言方法
- 扩展性:简单易用的自定义断言机制
- 诊断性:详细的错误报告
- 现代化:全面支持 Java 8+ 特性
专家建议:
- 新项目直接采用 AssertJ 作为主要断言库
- 结合 JUnit5 + Mockito + AssertJ 构建现代测试栈
- 对复杂领域对象创建自定义断言
- 在 CI 流程中使用软断言收集所有测试错误
- 优先使用
extracting()
而非多次调用get()
完整测试栈示例:
@ExtendWith(MockitoExtension.class)
class ModernTestStackExample {
@Mock
private Repository repository;
@InjectMocks
private Service service;
@Test
void testBusinessLogic() {
// Given
given(repository.findData()).willReturn(new Data("value"));
// When
Result result = service.process();
// Then
assertThat(result)
.isNotNull()
.extracting(Result::status, Result::code)
.containsExactly(Status.SUCCESS, 200);
verify(repository).save(any());
}
}