第一章 为什么我们需要这份"代码保险单"

想象你正在做一个在线商城的促销系统。某天运营同学突发奇想:"把满200减50的阶梯优惠改成满199减60吧"。你修改代码并上线后,支付模块却频繁报错。经过4小时排查才发现:计算金额时误用了原始优惠规则。单元测试就像代码的"自动化质检员",它能帮你预防这类问题。

以用户积分功能为例,不用测试的代码像这样裸奔:

public class BonusService {
    public int calculate(int amount) {
        if (amount < 100) return amount;
        return amount * 2; // 这里埋着BUG!
    }
}

如果有单元测试守护:

// 技术栈:JUnit 5 + Mockito
class BonusServiceTest {
    @Test
    @DisplayName("当消费98元时应获得98积分")
    void normalCase() {
        BonusService service = new BonusService();
        assertEquals(98, service.calculate(98));
    }

    @Test
    void over100Case() {
        BonusService service = new BonusService();
        assertNotEquals(200, service.calculate(100)); // 这里会失败!
    }
}

(注释:通过具体案例对比展示测试的防御价值)

第二章 JUnit 5新特性实战指南

2.1 让你的测试代码更有表现力

class EmployeeServiceTest {
    @Test
    @Tag("critical") // 标记重要测试集
    @DisplayName("验证新员工是否能生成工号")
    void testGenerateEmployeeId() {
        // Arrange
        EmployeeService service = new EmployeeService();
        
        // Act
        String id = service.generateId("张伟");
        
        // Assert
        assertTrue(id.startsWith("ZW"), 
            "工号应以姓名首字母开头");
        assertEquals(10, id.length());
    }
}

2.2 参数化测试实践

@ParameterizedTest
@CsvSource({
    "0, 0",
    "99, 99",
    "100, 200",
    "200, 400"
})
void testCalculateWithParams(int input, int expected) {
    BonusService service = new BonusService();
    assertEquals(expected, service.calculate(input));
}

2.3 动态测试新体验

@TestFactory
Stream<DynamicTest> dynamicTests() {
    List<Integer> inputs = Arrays.asList(50, 99, 100, 150);
    return inputs.stream().map(input ->
        DynamicTest.dynamicTest("测试金额:" + input, 
            () -> {
                BonusService service = new BonusService();
                int actual = service.calculate(input);
                assertTrue(actual >= input);
            }
        ));
}

第三章 Mockito核心技巧破译

3.1 何时需要Mock对象

当测试用户注册服务时,我们不希望真的发送短信:

class RegistrationServiceTest {
    @Mock
    SmsSender smsSender;

    @Test
    void shouldSendWelcomeSms() {
        // 创建Mock对象
        MockitoAnnotations.openMocks(this);
        
        UserService service = new UserService(smsSender);
        service.register("13800138000");
        
        verify(smsSender).send("13800138000", "欢迎注册!");
    }
}

3.2 Spy与Mock的双生子差异

@Test
void testOrderProcessor() {
    // 真实物流服务实例
    LogisticsService realService = new LogisticsService();
    
    // 创建Spy对象
    LogisticsService spy = spy(realService);
    
    OrderProcessor processor = new OrderProcessor(spy);
    processor.processOrder(new Order());
    
    // 验证实际调用
    verify(spy).calculateFee(any());
}

3.3 复杂场景的模拟策略

@Test
void testPaymentRetry() {
    PaymentGateway mockGateway = mock(PaymentGateway.class);
    
    // 第一次调用抛异常,第二次成功
    when(mockGateway.pay(any()))
        .thenThrow(new TimeoutException())
        .thenReturn("SUCCESS");
    
    PaymentService service = new PaymentService(mockGateway);
    String result = service.retryPayment();
    
    assertEquals("SUCCESS", result);
    verify(mockGateway, times(2)).pay(any());
}

第四章 高手必备的深度优化技巧

4.1 测试命名的最佳实践

@DisplayName("用户信息服务异常场景验证")
class UserServiceExceptionTest {
    @Test
    @DisplayName("当用户ID不存在时抛出NotFoundException")
    void throwWhenUserNotFound() {
        // ...异常测试逻辑
    }
}

4.2 时间敏感的测试处理

@Test
void testCacheExpiration() {
    CacheManager cache = new CacheManager();
    cache.put("key", "value", 1);
    
    // 使用Awaitility处理异步验证
    await().atMost(2, SECONDS)
           .untilAsserted(() -> 
               assertNull(cache.get("key")));
}

第五章 避坑指南与最佳实践

5.1 典型Mock误用场景

// 错误示例:过度模拟
@Test
void wrongMockUsage() {
    UserService mockService = mock(UserService.class);
    when(mockService.findUser(any())).thenReturn(new User());
    
    // 实际应该测试的业务逻辑
    // 被掩盖在过多的mock设定中
}

5.2 测试金字塔的构建策略

(文本说明分层测试策略)

第六章 技术全景分析

技术维度 JUnit 5优势 Mockito优势
扩展性 插件化架构支持 链式API设计
学习曲线 渐进式难度 直观的API命名
社区生态 企业级项目广泛采用 最受欢迎的Java Mock框架

第七章 应用场景矩阵

  1. JUnit 5首选场景

    • 基础逻辑验证
    • 多条件参数测试
    • 异步操作验证
  2. Mockito出击时刻

    • 外部服务依赖隔离
    • 异常条件模拟
    • 耗时操作替换

第八章 总结启示

经过三个小时的真实项目级测试代码演练,我们已经建立可靠的测试防线。当你下次面对需求变更时,这些自动化测试就像精密的安检仪,能帮你拦截90%的潜在问题。记住,好的测试不是负担,而是加速开发的推进器。