AssertJ 深度详解

AssertJ 深度详解

AssertJ 是 Java 领域最强大的流式断言库,提供超过 2000 种断言方法,通过链式调用实现高度可读的测试验证。与传统的 JUnit 断言相比,AssertJ 提供更丰富的功能和更优雅的语法。

一、核心优势与架构

AssertJ
流式API
丰富断言
错误信息
扩展机制
链式调用
集合/日期/异常
详细错误描述
自定义断言

二、环境配置(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());
    }
}

十三、最佳实践总结

  1. 链式调用原则

    // 推荐:单断言链
    assertThat(object)
        .isNotNull()
        .extracting(...)
        .satisfies(...);
    
    // 避免:多个独立断言
    assertNotNull(object);
    assertEquals(value, object.getField());
    
  2. 测试结构优化

    // 使用提取和条件
    assertThat(users)
        .filteredOn("active", true)
        .extracting("name", "email")
        .containsExactly(
            tuple("Alice", "alice@example.com"),
            tuple("Bob", "bob@example.com")
        );
    
  3. 错误信息增强

    assertThat(actual)
        .withFailMessage("期望值 %s 但实际是 %s", expected, actual)
        .isEqualTo(expected);
    
  4. 集合断言优先级

    大小检查
    包含检查
    顺序检查
    元素属性

十四、AssertJ 模块生态

模块功能依赖
assertj-core核心断言库Java 8+
assertj-guavaGuava 集合支持Guava 依赖
assertj-db数据库断言数据库连接
assertj-androidAndroid 扩展Android SDK
assertj-swingGUI 测试支持Swing 应用
assertj-joda-timeJoda-Time 支持Joda-Time 库

十五、与 JUnit 断言对比

场景JUnitAssertJ
对象相等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 核心价值

  1. 可读性:自然语言般的链式调用
  2. 丰富性:超过 2000 种断言方法
  3. 扩展性:简单易用的自定义断言机制
  4. 诊断性:详细的错误报告
  5. 现代化:全面支持 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());
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值