本文目录导读:

测试金字塔与分层设计
单元测试(Unit Test)
- 目标:测试最小代码单元(方法、类)
- 工具:JUnit 5, Mockito, AssertJ
- 特点:快速、隔离、无外部依赖
集成测试(Integration Test)
- 目标:验证模块间交互(数据库、API、消息队列)
- 工具:Testcontainers, Spring Boot Test, H2
- 特点:需要外部资源,验证边界
端到端测试(E2E Test)
- 目标:模拟用户真实操作
- 工具:Selenium, Cypress, REST Assured
- 特点:最慢但最接近真实场景
测试用例设计方法
等价类划分法
将输入数据划分为有效等价类和无效等价类,每个类取一个代表值。
示例:年龄字段(0-150岁)
// 有效等价类
@Test
void validAgeRange() {
assertDoesNotThrow(() -> new User(25));
}
// 无效等价类
@Test
void ageTooLow() {
assertThrows(IllegalArgumentException.class, () -> new User(-1));
}
@Test
void ageTooHigh() {
assertThrows(IllegalArgumentException.class, () -> new User(200));
}
边界值分析法
关注等价类的边界值(最小值、最大值、略小于/大于边界)。
示例:密码长度(8-20位)
// 边界值:7, 8, 20, 21
@Test
void passwordLengthBoundary() {
assertThrows(IllegalArgumentException.class, () -> new Password("1234567"));
assertDoesNotThrow(() -> new Password("12345678"));
assertDoesNotThrow(() -> new Password("12345678901234567890"));
assertThrows(IllegalArgumentException.class, () -> new Password("123456789012345678901"));
}
因果图法
用因果关系图描述输入条件间的组合,适合复杂业务逻辑。
示例:优惠券系统(新用户 + 订单满100)→ 可用
@Test
void couponApplicableWhenNewUserAndOrderOver100() {
User user = new User(true, false);
Order order = new Order(new BigDecimal("150"));
assertTrue(CouponService.isApplicable(user, order));
}
@Test
void couponNotApplicableWhenExistingUser() {
User user = new User(false, false);
Order order = new Order(new BigDecimal("150"));
assertFalse(CouponService.isApplicable(user, order));
}
正交实验法
用正交表减少全组合测试用例数,适合多条件场景。
示例:搜索功能(关键词、排序、过滤)
// 全组合:3x2x2=12,正交表可选4个
@ParameterizedTest
@CsvSource({
"Java, price_asc, fiction",
"Python, price_desc, non_fiction",
"Java, price_desc, fiction",
"Python, price_asc, non_fiction"
})
void searchWithDifferentCombinations(String keyword, String sort, String category) {
List<Book> results = searchService.search(keyword, sort, category);
assertNotNull(results);
}
场景测试法
基于用户故事或业务流程设计测试场景。
示例:用户注册流程
@Test
void successfulUserRegistrationFlow() {
// Step 1: 提交注册信息
RegistrationResponse resp1 = client.register("test@example.com", "Password123!");
assertTrue(resp1.isSuccess());
// Step 2: 验证邮箱
VerificationResponse resp2 = client.verifyEmail(resp1.getToken());
assertTrue(resp2.isVerified());
// Step 3: 登录
LoginResponse resp3 = client.login("test@example.com", "Password123!");
assertNotNull(resp3.getToken());
}
Java单元测试最佳实践
标准测试结构(AAA Pattern)
- Arrange:准备测试数据
- Act:执行被测方法
- Assert:验证结果
@Test
void calculateDiscount_WhenPremiumUserAndOrderOver100_ShouldReturn20Percent() {
// Arrange
User user = new User(UserType.PREMIUM);
Order order = new Order(new BigDecimal("150.00"));
// Act
BigDecimal discount = discountService.calculateDiscount(user, order);
// Assert
assertEquals(new BigDecimal("0.20"), discount);
}
使用Mockito隔离依赖
@Test
void orderService_WhenProductNotAvailable_ShouldThrowException() {
// Mock仓库服务
ProductRepository repo = mock(ProductRepository.class);
when(repo.findBySku("BOOK-001")).thenReturn(Optional.empty());
// 注入mock
OrderService service = new OrderService(repo);
// 执行并验证
assertThrows(ProductNotFoundException.class,
() -> service.createOrder("BOOK-001", 1));
verify(repo).findBySku("BOOK-001");
}
参数化测试
@ParameterizedTest
@ValueSource(ints = {0, 1, 10, 100})
void calculateShipping_WhenQuantityValid_ShouldReturnPositive(int quantity) {
BigDecimal shipping = shippingService.calculate(quantity);
assertTrue(shipping.compareTo(BigDecimal.ZERO) > 0);
}
@CsvSource({
"2, 5.99",
"5, 8.99",
"10, 12.99"
})
void calculateShipping_WhenQuantityChange_ShouldReturnCorrectAmount(int quantity, String expected) {
BigDecimal shipping = shippingService.calculate(quantity);
assertEquals(new BigDecimal(expected), shipping);
}
测试异常
// 验证异常类型和消息
@Test
void createOrder_WhenInvalidEmail_ShouldThrowException() {
IllegalArgumentException ex = assertThrows(
IllegalArgumentException.class,
() -> userService.createUser("invalid-email", "pass123")
);
assertTrue(ex.getMessage().contains("Invalid email format"));
}
常用测试工具与框架
| 工具 | 用途 | 示例 |
|---|---|---|
| JUnit 5 | 单元测试框架 | @Test, @ParameterizedTest |
| Mockito | Mock和Stub | mock(), when().thenReturn() |
| AssertJ | 流畅断言 | assertThat(result).isNotNull() |
| Testcontainers | 集成测试容器 | @Container PostgreSQLContainer |
| REST Assured | API测试 | given().get("/api/books").then() |
| Selenium | UI自动化 | WebDriver.findElement(By.id("submit")) |
测试设计检查清单
✅ 功能性测试
- [ ] 正常业务流程通过
- [ ] 边界值全覆盖
- [ ] 异常路径处理
- [ ] 数据完整性验证
✅ 非功能性测试
- [ ] 性能指标(响应时间<200ms)
- [ ] 并发安全性(多线程场景)
- [ ] 数据一致性(事务回滚)
- [ ] 缓存命中/失效
✅ 代码质量
- [ ] 测试覆盖率>80%
- [ ] 无重复测试逻辑
- [ ] 测试独立可执行
- [ ] 测试数据可清理
实战案例:用户订单系统
需求
- 用户创建订单时验证库存
- 黄金会员享受10%折扣
- 订单超过1000元免运费
测试用例设计
public class OrderServiceTest {
@Mock
private InventoryClient inventoryClient;
@Mock
private UserRepository userRepo;
@InjectMocks
private OrderService orderService;
// 1. 正常创建黄金会员订单
@Test
void createOrder_GoldMemberWithSufficientStock_ShouldApplyDiscount() {
User user = new User(1L, UserType.GOLD);
when(userRepo.findById(1L)).thenReturn(Optional.of(user));
when(inventoryClient.checkStock("BOOK-1", 2)).thenReturn(true);
Order order = orderService.createOrder(1L, "BOOK-1", 2);
assertEquals(new BigDecimal("180.00"), order.getTotal()); // 100*2*0.9
assertEquals(new BigDecimal("0.00"), order.getShipping()); // 免运费
}
// 2. 库存不足场景
@Test
void createOrder_WhenStockInsufficient_ShouldThrowException() {
when(inventoryClient.checkStock("BOOK-1", 10)).thenReturn(false);
assertThrows(InsufficientStockException.class,
() -> orderService.createOrder(1L, "BOOK-1", 10));
}
// 3. 普通会员订单,运费计算
@Test
void createOrder_NormalMemberUnder1000_ShouldChargeShipping() {
User user = new User(1L, UserType.NORMAL);
when(userRepo.findById(1L)).thenReturn(Optional.of(user));
when(inventoryClient.checkStock("BOOK-1", 1)).thenReturn(true);
Order order = orderService.createOrder(1L, "BOOK-1", 1);
assertEquals(new BigDecimal("100.00"), order.getTotal()); // 原价
assertEquals(new BigDecimal("10.00"), order.getShipping()); // 运费
}
// 4. 并发下单测试
@Test
void createOrder_ConcurrentAccess_ShouldHandleCorrectly() throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(5);
CountDownLatch latch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
executor.submit(() -> {
try {
orderService.createOrder(1L, "LIMITED-BOOK", 1);
} finally {
latch.countDown();
}
});
}
latch.await();
// 验证库存只扣减1次
verify(inventoryClient, times(1)).deductStock("LIMITED-BOOK", 1);
}
}
文档与维护
- 测试命名:
[MethodName]_[Scenario]_[ExpectedResult](如calculateDiscount_PremiumUser_Returns20Percent) - Gherkin风格(适用于集成测试):
Scenario: 黄金会员下单免运费 Given 用户是黄金会员 And 商品库存充足 When 用户下单金额超过1000元 Then 订单会免运费
- 注释规范:仅需在复杂逻辑处添加说明
- 定期review:每季度检查测试用例是否反映最新业务规则