第一章 为什么我们需要这份"代码保险单"
想象你正在做一个在线商城的促销系统。某天运营同学突发奇想:"把满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框架 |
第七章 应用场景矩阵
JUnit 5首选场景
- 基础逻辑验证
- 多条件参数测试
- 异步操作验证
Mockito出击时刻
- 外部服务依赖隔离
- 异常条件模拟
- 耗时操作替换
第八章 总结启示
经过三个小时的真实项目级测试代码演练,我们已经建立可靠的测试防线。当你下次面对需求变更时,这些自动化测试就像精密的安检仪,能帮你拦截90%的潜在问题。记住,好的测试不是负担,而是加速开发的推进器。
评论