Mockito + AssertJ 深度集成指南

Mockito + AssertJ 深度集成指南:流畅测试的艺术

Mockito 和 AssertJ 是现代 Java 测试的完美组合,提供了模拟对象流畅断言的强大能力。本文将深入探讨如何将两者无缝结合,创建可读性强、维护性高的测试代码。

一、核心优势与架构设计

Mockito
对象模拟
行为验证
AssertJ
流畅断言
丰富匹配器
集成价值
单一职责测试
链式表达
自描述测试

核心优势

  • 表达力:链式调用使测试代码如自然语言般流畅
  • 可读性:自描述断言降低理解成本
  • 诊断信息:详细的错误报告加速问题定位
  • 扩展性:支持自定义断言和匹配器
  • 类型安全:减少运行时错误

二、Maven 依赖配置

<dependencies>
    <!-- JUnit5 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.9.0</version>
        <scope>test</scope>
    </dependency>
    
    <!-- Mockito -->
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>4.11.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-junit-jupiter</artifactId>
        <version>4.11.0</version>
        <scope>test</scope>
    </dependency>
    
    <!-- AssertJ -->
    <dependency>
        <groupId>org.assertj</groupId>
        <artifactId>assertj-core</artifactId>
        <version>3.23.1</version>
        <scope>test</scope>
    </dependency>
</dependencies>

三、基础集成模式

1. 标准测试结构

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserService userService;
    
    @Test
    void registerUser_Success() {
        // 1. Mock 行为设置
        when(userRepository.save(any(User.class)))
            .thenAnswer(invocation -> {
                User user = invocation.getArgument(0);
                user.setId(1L); // 模拟数据库ID生成
                return user;
            });
        
        // 2. 执行测试方法
        User result = userService.registerUser("john", "john@example.com");
        
        // 3. AssertJ 验证结果
        assertThat(result)
            .isNotNull()
            .extracting(User::getId, User::getUsername, User::getEmail)
            .containsExactly(1L, "john", "john@example.com");
        
        // 4. Mockito 验证行为
        verify(userRepository).save(any(User.class));
        verifyNoMoreInteractions(userRepository);
    }
}

2. BDD 风格测试

@Test
void registerUser_BDDStyle() {
    // Given
    given(userRepository.save(any(User.class)))
        .willAnswer(invocation -> {
            User user = invocation.getArgument(0);
            user.setId(1L);
            return user;
        });
    
    // When
    User result = userService.registerUser("alice", "alice@example.com");
    
    // Then
    then(userRepository).should().save(any(User.class));
    
    assertThat(result)
        .as("注册用户应返回有效用户对象")
        .isNotNull()
        .satisfies(user -> {
            assertThat(user.getId()).isPositive();
            assertThat(user.getUsername()).isEqualTo("alice");
            assertThat(user.getEmail()).contains("@");
        });
}

四、高级集成技巧

1. 集合断言与模拟验证

@Test
void findActiveUsers_ShouldReturnFilteredResults() {
    // 准备测试数据
    User activeUser = new User(1L, "active", "active@test.com", true);
    User inactiveUser = new User(2L, "inactive", "inactive@test.com", false);
    
    // 配置Mock行为
    when(userRepository.findAll()).thenReturn(List.of(activeUser, inactiveUser));
    
    // 执行测试
    List<User> activeUsers = userService.findActiveUsers();
    
    // AssertJ 集合断言
    assertThat(activeUsers)
        .hasSize(1)
        .extracting(User::getUsername)
        .containsExactly("active");
    
    // Mockito 验证
    verify(userRepository).findAll();
    verifyNoMoreInteractions(userRepository);
}

2. 异常断言与行为验证

@Test
void activateUser_NonExistingUser_ThrowsException() {
    // 配置Mock行为
    when(userRepository.findById(999L)).thenReturn(Optional.empty());
    
    // AssertJ 异常断言
    assertThatThrownBy(() -> userService.activateUser(999L))
        .isInstanceOf(UserNotFoundException.class)
        .hasMessageContaining("User not found")
        .hasNoCause();
    
    // Mockito 验证
    verify(userRepository).findById(999L);
    verifyNoInteractions(userRepository);
}

3. 参数捕获与深度断言

@Captor
private ArgumentCaptor<User> userCaptor;

@Test
void registerUser_CapturesCorrectData() {
    // 执行测试
    userService.registerUser("bob", "bob@example.com");
    
    // 捕获参数
    verify(userRepository).save(userCaptor.capture());
    User capturedUser = userCaptor.getValue();
    
    // AssertJ 深度断言
    assertThat(capturedUser)
        .extracting(User::getUsername, User::getEmail, User::isActive)
        .containsExactly("bob", "bob@example.com", false);
    
    // 使用条件断言
    assertThat(capturedUser).matches(user -> 
        user.getUsername().length() >= 3 && 
        user.getEmail().contains("@")
    );
}

4. 时间相关断言

@Test
void processOrder_ShouldCompleteInTime() {
    // 配置慢速服务
    when(paymentService.process(any())).thenAnswer(invocation -> {
        Thread.sleep(50); // 模拟处理延迟
        return true;
    });
    
    // 执行并验证性能
    assertThatCode(() -> orderService.processOrder(new Order()))
        .doesNotThrowAnyException()
        .satisfies(result -> {
            // 时间断言
            assertThatExecutionTime()
                .of(() -> orderService.processOrder(new Order()))
                .isLessThan(100, TimeUnit.MILLISECONDS);
        });
}

五、自定义扩展

1. 自定义 AssertJ 断言

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 hasValidEmail() {
        isNotNull();
        if (!actual.getEmail().contains("@")) {
            failWithMessage("用户邮箱格式无效: %s", actual.getEmail());
        }
        return this;
    }
    
    public UserAssert isActive() {
        isNotNull();
        if (!actual.isActive()) {
            failWithMessage("用户应处于激活状态");
        }
        return this;
    }
}

// 使用自定义断言
@Test
void customAssertionUsage() {
    User user = userService.activateUser(1L);
    
    UserAssert.assertThat(user)
        .isActive()
        .hasValidEmail();
}

2. 自定义 Mockito Answer

@Test
void registerUser_WithCustomAnswer() {
    // 自定义Answer收集数据
    List<User> registeredUsers = new ArrayList<>();
    
    when(userRepository.save(any(User.class)))
        .thenAnswer(invocation -> {
            User user = invocation.getArgument(0);
            registeredUsers.add(user);
            user.setId((long) registeredUsers.size());
            return user;
        });
    
    // 执行测试
    userService.registerUser("test", "test@example.com");
    
    // AssertJ 验证自定义Answer
    assertThat(registeredUsers)
        .hasSize(1)
        .first()
        .extracting(User::getUsername, User::getEmail)
        .containsExactly("test", "test@example.com");
}

六、最佳实践

1. 测试结构优化(AAA 模式)

@Test
void optimalTestStructure() {
    // Arrange: 准备测试数据和Mock行为
    User expectedUser = new User(1L, "arranged", "arranged@test.com");
    when(userRepository.findById(1L)).thenReturn(Optional.of(expectedUser));
    
    // Act: 执行测试方法
    User result = userService.getUser(1L);
    
    // Assert: 使用AssertJ验证结果
    assertThat(result)
        .usingRecursiveComparison()
        .ignoringFields("lastLogin")
        .isEqualTo(expectedUser);
    
    // Verify: 使用Mockito验证行为
    verify(userRepository).findById(1L);
}

2. 断言优先顺序

空值检查
类型检查
关键属性
集合内容
完整对象
交互验证

3. 组合断言策略

@Test
void combinedAssertions() {
    // 执行复杂操作
    Report report = reportService.generateMonthlyReport(2023, 6);
    
    // 组合断言
    assertThat(report)
        .as("2023年6月月度报告")
        .isNotNull()
        .satisfies(r -> {
            // 财务数据验证
            assertThat(r.getFinancialData())
                .extracting(FinancialData::getRevenue, FinancialData::getProfit)
                .containsExactly(BigDecimal.valueOf(150000), BigDecimal.valueOf(35000));
            
            // 部门表现验证
            assertThat(r.getDepartmentPerformance())
                .filteredOn(dept -> dept.getRating() > 8)
                .extracting(DepartmentPerformance::getName)
                .containsExactly("Sales", "Engineering");
        });
    
    // 验证服务调用
    verify(reportGenerator).generate(anyInt(), anyInt());
}

七、企业级应用场景

1. REST API 测试

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;
    
    @Test
    void getUserById_ShouldReturnUser() throws Exception {
        // 准备Mock数据
        User mockUser = new User(1L, "api_user", "api@test.com");
        when(userService.getUser(1L)).thenReturn(mockUser);
        
        // 执行请求
        MvcResult result = mockMvc.perform(get("/users/1"))
            .andExpect(status().isOk())
            .andReturn();
        
        // 提取响应
        String json = result.getResponse().getContentAsString();
        
        // AssertJ 验证JSON响应
        assertThatJson(json)
            .isObject()
            .containsEntry("id", 1)
            .containsEntry("username", "api_user")
            .node("email").isString().contains("@");
    }
}

2. 异步服务测试

@Test
void asyncProcessing_ShouldCompleteSuccessfully() {
    // 配置异步Mock
    when(asyncService.process(any()))
        .thenReturn(CompletableFuture.completedFuture("success"));
    
    // 执行测试
    CompletableFuture<String> future = userService.asyncOperation("data");
    
    // AssertJ 异步断言
    assertThat(future)
        .succeedsWithin(1, TimeUnit.SECONDS)
        .isEqualTo("success");
}

3. 数据驱动测试

@ParameterizedTest
@CsvSource({
    "john, john@example.com, true",
    "invalid, bad-email, false",
    "short, a@b.c, false"
})
void registerUser_ValidationCases(String username, String email, boolean valid) {
    // 配置Mock
    if (valid) {
        when(userRepository.save(any())).thenReturn(new User(1L, username, email));
    }
    
    if (valid) {
        User result = userService.registerUser(username, email);
        assertThat(result).isNotNull();
    } else {
        assertThatThrownBy(() -> userService.registerUser(username, email))
            .isInstanceOf(ValidationException.class);
    }
}

八、性能优化

1. 共享 Mock 初始化

@BeforeEach
void setUp() {
    // 公共Mock配置
    when(userRepository.save(any(User.class)))
        .thenAnswer(invocation -> {
            User user = invocation.getArgument(0);
            user.setId(ThreadLocalRandom.current().nextLong(1, 1000));
            return user;
        });
}

2. 软断言收集所有错误

@Test
void multipleValidationsWithSoftAssertions() {
    // 创建软断言对象
    SoftAssertions softly = new SoftAssertions();
    
    // 执行操作
    User user = userService.registerUser("test", "test@example.com");
    
    // 多个验证点
    softly.assertThat(user.getId()).as("ID").isPositive();
    softly.assertThat(user.getUsername()).as("Username").isEqualTo("test");
    softly.assertThat(user.getEmail()).as("Email").contains("@");
    softly.assertThat(user.isActive()).as("Active status").isFalse();
    
    // 验证交互
    verify(userRepository).save(any(User.class));
    
    // 统一报告所有失败
    softly.assertAll();
}

九、常见问题解决方案

问题解决方案
断言失败信息不足使用 as() 添加描述性信息
集合内容验证困难使用 extracting().containsExactly()
复杂对象比较usingRecursiveComparison() 递归比较
Mock 行为未触发检查方法签名匹配(参数、类型)
异步测试超时使用 succeedsWithin() 指定超时
嵌套属性验证复杂自定义断言类封装验证逻辑

十、总结

Mockito + AssertJ 核心价值

  1. 流畅表达:链式调用创建可读性强的测试代码
  2. 精准验证:结合模拟行为和结果断言全面覆盖
  3. 诊断友好:详细错误信息加速问题定位
  4. 扩展灵活:支持自定义断言和匹配器
  5. 现代语法:充分利用 Java 8+ 特性

最佳实践建议

  • 优先使用 BDD 风格(Given-When-Then)
  • 为复杂领域对象创建自定义断言
  • 使用软断言收集多个验证点错误
  • 结合参数化测试覆盖边界条件
  • 在持续集成中强制执行测试覆盖率

完整测试示例

@ExtendWith(MockitoExtension.class)
class ComprehensiveTest {

    @Mock
    private OrderRepository orderRepository;
    
    @Mock
    private PaymentGateway paymentGateway;
    
    @InjectMocks
    private OrderService orderService;
    
    @Test
    void placeOrder_ComplexScenario() {
        // Given: 准备测试数据
        Order order = new Order(LocalDateTime.now(), List.of(
            new OrderItem("A001", 2, 25.0),
            new OrderItem("B002", 1, 100.0)
        ));
        
        // 配置Mock行为
        when(orderRepository.save(any())).thenAnswer(inv -> {
            Order o = inv.getArgument(0);
            o.setId(1001L);
            return o;
        });
        when(paymentGateway.process(anyDouble())).thenReturn("PAY-12345");
        
        // When: 执行测试方法
        OrderResult result = orderService.placeOrder(order);
        
        // Then: AssertJ 验证结果
        assertThat(result)
            .isNotNull()
            .extracting(OrderResult::getOrderId, OrderResult::getStatus)
            .containsExactly(1001L, OrderStatus.CONFIRMED);
            
        assertThat(result.getPaymentId())
            .as("支付ID格式")
            .startsWith("PAY-")
            .hasSizeGreaterThan(8);
            
        // And: 验证交互行为
        verify(paymentGateway).process(eq(150.0));
        verify(orderRepository).save(same(order));
    }
}

Mockito 和 AssertJ 的结合为 Java 测试提供了强大的表达能力和验证机制。通过遵循本文的模式和实践,您可以创建更健壮、更易维护的测试套件,显著提升软件质量和开发效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值