# 测试最佳实践检查清单 > 基于蚊子项目1210+测试的真实经验总结 > > 本文档旨在帮助识别和规避AI生成测试中常见的陷阱,提升测试质量和可维护性。 ## 📋 目录 1. [AI测试陷阱识别清单](#1-ai测试陷阱识别清单) 2. [测试质量评估矩阵](#2-测试质量评估矩阵) 3. [可维护性检查清单](#3-可维护性检查清单) 4. [性能测试最佳实践](#4-性能测试最佳实践) 5. [并发测试策略](#5-并发测试策略) 6. [测试命名与文档规范](#6-测试命名与文档规范) 7. [Mock使用准则](#7-mock使用准则) 8. [断言最佳实践](#8-断言最佳实践) 9. [边界条件系统化方法](#9-边界条件系统化方法) 10. [持续改进检查表](#10-持续改进检查表) --- ## 1. AI测试陷阱识别清单 ### 1.1 过度测试框架本身 🚫 #### 问题描述 AI常生成测试Lombok生成的getter/setter、构造函数或框架代码的测试,这些测试毫无意义。 #### ❌ 错误示例 ```java @Test void testGetterSetter() { User user = new User(); user.setName("test"); assertEquals("test", user.getName()); // 测试Lombok生成的方法 } @Test void testConstructor() { User user = new User("id", "name"); assertNotNull(user); // 测试对象能被创建 assertEquals("id", user.getId()); // 测试简单赋值 } ``` #### ✅ 正确示例 ```java @Test void shouldDetectDuplicateUsers_whenRegisteringWithExistingEmail() { // 测试业务逻辑,而非框架功能 userRepository.save(new User("existing@test.com")); DuplicateException exception = assertThrows( DuplicateException.class, () -> userService.register(new RegistrationRequest("existing@test.com")) ); assertEquals("USER_EXISTS", exception.getErrorCode()); } ``` #### 检查方法 - [ ] 是否测试了框架生成的代码? - [ ] 是否测试了无业务逻辑的简单赋值? - [ ] 是否测试了编译器会保证的行为? #### 自动化规则 ```bash # 查找可疑的DTO测试 grep -r "testGetter\|testSetter\|testConstructor" src/test --include="*.java" # 查找Lombok类的无意义测试 find src/main -name "*.java" -exec grep -l "@Data\|@Getter\|@Setter" {} \; | \ xargs -I {} basename {} .java | \ xargs -I {} find src/test -name "{}Test.java" -exec grep -l "get.*()\|set.*()" {} \; ``` --- ### 1.2 虚假断言 🚫 #### 问题描述 断言永远不会失败,或只验证显而易见的事情,无法真正验证业务逻辑。 #### ❌ 错误示例 ```java @Test void testCreateObject() { Object obj = new Object(); assertNotNull(obj); // 永远不会失败 } @Test void testTrueIsTrue() { assertTrue(true); // 无意义断言 } @Test void testServiceCalled() { service.doSomething(); verify(service).doSomething(); // 只验证方法被调用 } ``` #### ✅ 正确示例 ```java @Test void shouldCalculateCorrectReward_whenUserCompletesHighValueActivity() { // Given Activity activity = Activity.builder() .rewardPoints(100) .multiplier(2.0) .build(); // When Reward reward = rewardCalculator.calculate(activity, user); // Then assertEquals(200, reward.getPoints()); // 验证实际业务值 assertEquals("PREMIUM", reward.getTier()); // 验证业务状态 assertNotNull(reward.getAwardedAt()); // 验证副作用 } ``` #### 检查方法 - [ ] 断言是否可能失败? - [ ] 是否验证了具体的业务值? - [ ] 是否验证了状态变化? #### 自动化规则 ```bash # 查找虚假断言 grep -r "assertNotNull(new\|assertTrue(true\|assertFalse(false)" src/test --include="*.java" # 查找只验证调用的测试 grep -r "verify(.*)\.doSomething\|verify(.*)\.save\|verify(.*)\.find" src/test --include="*.java" | \ grep -v "assert\|verify.*times" ``` --- ## 2. 测试质量评估矩阵 ### 2.1 质量维度评分表 | 维度 | 权重 | 优秀(5) | 良好(4) | 合格(3) | 较差(2) | 不合格(1) | |------|------|---------|---------|---------|---------|-----------| | **业务价值** | 30% | 验证核心业务流程 | 验证重要业务规则 | 验证一般功能 | 验证边缘功能 | 无业务价值 | | **断言质量** | 25% | 多维度精确验证 | 验证具体业务值 | 基本断言 | 模糊断言 | 虚假断言 | | **边界覆盖** | 20% | 全面边界测试 | 主要边界覆盖 | 部分边界 | 极少边界 | 无边界测试 | | **可读性** | 15% | 自文档化+注释 | 清晰命名结构 | 基本可读 | 难以理解 | 无法维护 | | **独立性** | 10% | 完全独立 | 偶尔共享setup | 部分依赖 | 强依赖 | 相互依赖 | ### 2.2 测试分级标准 ``` A级 (90-100分): 生产级测试,值得作为范例 B级 (75-89分): 良好测试,可以接受 C级 (60-74分): 及格测试,需要改进 D级 (40-59分): 问题测试,必须重构 F级 (<40分): 无效测试,应该删除 ``` ### 2.3 评估检查清单 - [ ] 测试是否明确验证了业务需求? - [ ] 失败时能否快速定位问题? - [ ] 是否覆盖了正常、异常、边界三种情况? - [ ] 新开发者能否理解测试意图? - [ ] 修改被测代码后,测试能否正确失败? - [ ] 测试执行时间是否合理(<1秒)? - [ ] 是否存在测试间的隐式依赖? ### 2.4 自动化评估脚本 ```bash #!/bin/bash # test-quality-check.sh echo "=== 测试质量快速评估 ===" # 1. 检查虚假断言 echo "1. 虚假断言检查:" grep -r "assertTrue(true)\|assertFalse(false)\|assertNotNull(new" src/test --include="*.java" | wc -l echo " 发现疑似虚假断言数量" # 2. 检查getter/setter测试 echo "2. Getter/Setter测试检查:" grep -r "testGetter\|testSetter\|getId()\|setName" src/test --include="*.java" | wc -l echo " 发现疑似框架测试数量" # 3. 检查测试复杂度 echo "3. 测试复杂度检查:" find src/test -name "*Test.java" -exec wc -l {} \; | sort -n | tail -5 echo " 最长的5个测试文件" # 4. 检查断言密度 echo "4. 断言密度检查:" for file in $(find src/test -name "*Test.java" | head -20); do tests=$(grep -c "@Test" "$file" 2>/dev/null || echo 0) asserts=$(grep -c "assert" "$file" 2>/dev/null || echo 0) if [ "$tests" -gt 0 ]; then density=$(echo "scale=2; $asserts / $tests" | bc) echo " $file: $asserts assertions / $tests tests = $density" fi done ``` --- ## 3. 可维护性检查清单 ### 3.1 硬编码值问题 #### 问题描述 测试中使用魔法数字和字符串,导致测试脆弱且难以理解。 #### ❌ 错误示例 ```java @Test void testCalculation() { assertEquals(1000, service.calculate(500, 2)); // 魔法数字 assertEquals("ERROR_001", exception.getCode()); // 无意义错误码 } ``` #### ✅ 正确示例 ```java class RewardCalculationTest { private static final int BASE_POINTS = 500; private static final int MULTIPLIER = 2; private static final int EXPECTED_REWARD = 1000; private static final String ERROR_INVALID_MULTIPLIER = "ERR_MULTIPLIER_NEGATIVE"; @Test void shouldCalculateReward_whenValidMultiplierProvided() { int result = service.calculate(BASE_POINTS, MULTIPLIER); assertEquals(EXPECTED_REWARD, result); } } ``` #### 检查方法 - [ ] 所有字面量是否有明确含义? - [ ] 是否使用了有意义的常量名? - [ ] 魔法数字是否分散在测试各处? --- ### 3.2 重复代码问题 #### 问题描述 多个测试中重复相同的setup和断言逻辑,难以维护。 #### ❌ 错误示例 ```java @Test void testCreateUser() { User user = new User(); user.setName("test"); user.setEmail("test@test.com"); user.setCreatedAt(LocalDateTime.now()); // ... 重复10次 } @Test void testUpdateUser() { User user = new User(); user.setName("test"); user.setEmail("test@test.com"); user.setCreatedAt(LocalDateTime.now()); // ... 同样的重复 } ``` #### ✅ 正确示例 ```java class UserServiceTest { private User testUser; @BeforeEach void setUp() { testUser = createDefaultUser(); } private User createDefaultUser() { return User.builder() .name("Test User") .email("test@test.com") .createdAt(LocalDateTime.now()) .build(); } private void assertUserHasDefaultValues(User user) { assertAll("User default values", () -> assertEquals("Test User", user.getName()), () -> assertEquals("test@test.com", user.getEmail()), () -> assertNotNull(user.getCreatedAt()) ); } } ``` #### 检查方法 - [ ] 是否使用了@BeforeEach/@BeforeAll? - [ ] 是否有测试数据工厂类? - [ ] 是否有共享的断言方法? --- ### 3.3 测试间依赖问题 #### 问题描述 测试之间存在隐式依赖,执行顺序影响结果,难以并行运行。 #### ❌ 错误示例 ```java @Test @Order(1) // 强制顺序本身就是问题信号 void testCreate() { userService.createUser("test"); } @Test @Order(2) // 依赖testCreate void testRead() { User user = userService.findByName("test"); // 依赖前面的测试数据 assertNotNull(user); } ``` #### ✅ 正确示例 ```java @Test void shouldCreateAndRetrieveUser_independently() { // Given - 每个测试自己准备数据 String uniqueName = "test_" + UUID.randomUUID(); // When userService.createUser(uniqueName); User created = userService.findByName(uniqueName); // Then assertNotNull(created); // Cleanup - 自己清理 userService.delete(created.getId()); } ``` #### 检查方法 - [ ] 是否使用了@Order注解?(危险信号) - [ ] 测试能否独立运行? - [ ] 是否共享可变状态? - [ ] 能否并行执行(`mvn test -Dparallel`)? --- ## 4. 性能测试最佳实践 ### 4.1 性能测试类型 ```java /** * 1. 基线测试 - 记录正常性能指标 */ @Test void shouldCompleteWithinBaseline_whenProcessingStandardLoad() { // Given List standardData = DataFactory.create(100); // 标准数据量 // When long start = System.currentTimeMillis(); Result result = processor.process(standardData); long duration = System.currentTimeMillis() - start; // Then assertTrue(duration < 500, // 基线: <500ms "Processing took " + duration + "ms, expected <500ms"); assertNotNull(result); } /** * 2. 负载测试 - 测试预期峰值 */ @Test void shouldHandlePeakLoad_whenConcurrentRequestsArrive() { int concurrentUsers = 100; int requestsPerUser = 10; ExecutorService executor = Executors.newFixedThreadPool(concurrentUsers); CountDownLatch latch = new CountDownLatch(concurrentUsers * requestsPerUser); AtomicInteger successCount = new AtomicInteger(0); // When for (int i = 0; i < concurrentUsers; i++) { executor.submit(() -> { for (int j = 0; j < requestsPerUser; j++) { try { Response response = api.call(); if (response.isSuccess()) { successCount.incrementAndGet(); } } finally { latch.countDown(); } } }); } // Then assertTrue(latch.await(30, TimeUnit.SECONDS)); assertEquals(concurrentUsers * requestsPerUser, successCount.get()); executor.shutdown(); } /** * 3. 压力测试 - 测试系统极限 */ @Test void shouldDegradeGracefully_whenExceedingCapacity() { // 逐步增加负载直到系统出现性能下降 for (int load : Arrays.asList(100, 500, 1000, 2000, 5000)) { PerformanceResult result = runWithLoad(load); if (result.getErrorRate() > 0.05) { // 5%错误率阈值 // 记录最大容量 log.info("System capacity limit: {} requests", load); assertTrue(result.isGracefulDegradation(), "System should degrade gracefully"); break; } } } /** * 4. 稳定性测试 - 长时间运行 */ @Test void shouldMaintainPerformance_overExtendedPeriod() { long testDuration = TimeUnit.MINUTES.toMillis(5); long startTime = System.currentTimeMillis(); List responseTimes = new ArrayList<>(); while (System.currentTimeMillis() - startTime < testDuration) { long callStart = System.currentTimeMillis(); api.call(); responseTimes.add(System.currentTimeMillis() - callStart); Thread.sleep(100); // 模拟真实间隔 } // 分析响应时间趋势 double avgResponseTime = responseTimes.stream() .mapToLong(Long::longValue) .average() .orElse(0); double p95ResponseTime = calculatePercentile(responseTimes, 95); assertTrue(avgResponseTime < 200, "Average response time too high"); assertTrue(p95ResponseTime < 500, "P95 response time too high"); } ``` ### 4.2 性能测试检查清单 - [ ] 是否建立了性能基线? - [ ] 是否测试了预期峰值负载? - [ ] 是否测试了系统极限(压力测试)? - [ ] 是否进行了长时间稳定性测试? - [ ] 是否测量了响应时间分布(P50, P95, P99)? - [ ] 是否监控了资源使用(CPU, 内存, IO)? - [ ] 是否有性能回归检测机制? ### 4.3 性能反模式 ```java // ❌ 在单元测试中测试性能 @Test void testPerformance() { // 错误:单元测试不该测性能 for (int i = 0; i < 1000000; i++) { service.doSomething(); } } // ❌ 不稳定的性能断言 @Test void testResponseTime() { long start = System.currentTimeMillis(); service.call(); long duration = System.currentTimeMillis() - start; assertTrue(duration < 10); // 太严格,环境差异会导致失败 } // ❌ 只测试吞吐量不测试延迟 @Test void testThroughput() { int count = 0; long end = System.currentTimeMillis() + 1000; while (System.currentTimeMillis() < end) { service.call(); count++; } assertTrue(count > 1000); // 只关心数量不关心单个请求质量 } ``` --- ## 5. 并发测试策略 ### 5.1 并发测试基本原则 ```java /** * 并发测试黄金法则: * 1. 不要依赖Thread.sleep() - 使用CountDownLatch/CyclicBarrier * 2. 不要假设执行顺序 - 使用同步原语控制 * 3. 测试竞争条件,而不仅仅是并发执行 * 4. 验证最终一致性和不变量 */ ``` ### 5.2 常见并发测试模式 ```java /** * 模式1: 测试竞态条件 - 库存扣减 */ @Test void shouldPreventOverselling_whenConcurrentPurchases() throws InterruptedException { // Given Product product = productRepository.save( Product.builder().stock(10).build() ); int concurrentBuyers = 15; ExecutorService executor = Executors.newFixedThreadPool(concurrentBuyers); CountDownLatch startLatch = new CountDownLatch(1); CountDownLatch completeLatch = new CountDownLatch(concurrentBuyers); AtomicInteger successCount = new AtomicInteger(0); AtomicInteger failCount = new AtomicInteger(0); // When - 所有线程同时开始 for (int i = 0; i < concurrentBuyers; i++) { executor.submit(() -> { try { startLatch.await(); // 等待统一信号 try { purchaseService.buy(product.getId(), 1); successCount.incrementAndGet(); } catch (OutOfStockException e) { failCount.incrementAndGet(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { completeLatch.countDown(); } }); } startLatch.countDown(); // 同时触发所有线程 completeLatch.await(10, TimeUnit.SECONDS); // Then - 验证不变量 Product updated = productRepository.findById(product.getId()).orElseThrow(); assertEquals(10, successCount.get(), "只有10个应该成功"); assertEquals(5, failCount.get(), "5个应该失败"); assertEquals(0, updated.getStock(), "库存应该为0"); } /** * 模式2: 测试死锁 */ @Test void shouldNotDeadlock_whenNestedLockAcquisition() { Account account1 = new Account("A1", 1000); Account account2 = new Account("A2", 1000); ExecutorService executor = Executors.newFixedThreadPool(2); // 两个线程以不同顺序获取锁(经典的死锁场景) Future future1 = executor.submit(() -> transferService.transfer(account1, account2, 100)); Future future2 = executor.submit(() -> transferService.transfer(account2, account1, 100)); // 验证不会死锁 assertDoesNotThrow(() -> { future1.get(5, TimeUnit.SECONDS); future2.get(5, TimeUnit.SECONDS); }); } /** * 模式3: 测试可见性 */ @Test void shouldEnsureVisibility_whenMultipleThreadsReadWrite() throws InterruptedException { ConcurrentProcessor processor = new ConcurrentProcessor(); int writerCount = 5; int readerCount = 10; int iterations = 1000; ExecutorService executor = Executors.newFixedThreadPool(writerCount + readerCount); CountDownLatch latch = new CountDownLatch(writerCount + readerCount); AtomicInteger visibilityViolations = new AtomicInteger(0); // Writers for (int i = 0; i < writerCount; i++) { final int writerId = i; executor.submit(() -> { for (int j = 0; j < iterations; j++) { processor.write(writerId, j); } latch.countDown(); }); } // Readers for (int i = 0; i < readerCount; i++) { executor.submit(() -> { for (int j = 0; j < iterations; j++) { Data data = processor.read(); // 验证读取的数据一致性 if (!data.isConsistent()) { visibilityViolations.incrementAndGet(); } } latch.countDown(); }); } latch.await(30, TimeUnit.SECONDS); assertEquals(0, visibilityViolations.get(), "不应该出现可见性违规"); } /** * 模式4: 使用JCStress进行系统并发测试 */ @JCStressTest @Outcome(id = "1, 1", expect = Expect.ACCEPTABLE, desc = "两者都看到完整写入") @Outcome(id = "0, 1", expect = Expect.ACCEPTABLE, desc = "一个看到写入") @Outcome(id = "1, 0", expect = Expect.ACCEPTABLE, desc = "另一个看到写入") @Outcome(id = "0, 0", expect = Expect.FORBIDDEN, desc = "都不看到写入是bug") @State public class VolatileVisibilityTest { volatile int x; volatile int y; @Actor public void actor1() { x = 1; y = 1; } @Actor public void actor2(II_Result r) { r.r1 = x; r.r2 = y; } } ``` ### 5.3 并发测试检查清单 - [ ] 是否测试了竞态条件? - [ ] 是否验证了不变量(最终一致性)? - [ ] 是否使用了同步原语而非Thread.sleep()? - [ ] 是否考虑了不同的线程交错? - [ ] 是否测试了超时场景? - [ ] 是否验证了资源清理? - [ ] 是否使用JCStress等工具进行压力测试? --- ## 6. 测试命名与文档规范 ### 6.1 测试命名最佳实践 #### 命名格式对比 | 格式 | 示例 | 推荐度 | |------|------|--------| | **should_when** | `shouldThrowException_whenInvalidInput` | ⭐⭐⭐⭐⭐ | | **Given_When_Then** | `GivenValidUser_WhenRegister_ThenSuccess` | ⭐⭐⭐⭐ | | **method_condition_result** | `registerUser_withInvalidEmail_throwsException` | ⭐⭐⭐⭐ | | **testMethod** | `testRegister` | ⭐⭐ 不推荐 | #### ✅ 优秀命名示例 ```java // 业务场景清晰 @Test void shouldSendNotification_whenUserCompletesMilestone() // 边界条件明确 @Test void shouldRejectRequest_whenEmailExceeds255Characters() // 异常场景具体 @Test void shouldThrowDuplicateKeyException_whenRegisteringExistingEmail() // 状态转换明确 @Test void shouldTransitionFromPendingToActive_whenPaymentConfirmed() // 并发场景清晰 @Test void shouldMaintainConsistency_whenConcurrentUpdatesToSameEntity() ``` #### ❌ 糟糕命名示例 ```java @Test void test1() // 无意义 @Test void testUser() // 过于笼统 @Test void testRegisterUserSuccess() // 没有when部分 @Test void test() // 完全无信息 ``` ### 6.2 Given-When-Then结构 ```java @Test void shouldCalculateDiscountedPrice_whenPremiumMemberOnSale() { // ==================== GIVEN ==================== // 准备测试数据 User premiumUser = User.builder() .id("user-123") .membershipTier(MembershipTier.PREMIUM) .registrationDate(LocalDate.of(2020, 1, 1)) .build(); Product saleProduct = Product.builder() .id("prod-456") .basePrice(new BigDecimal("100.00")) .saleDiscount(new BigDecimal("0.20")) // 8折 .build(); // 配置mock when(userService.findById("user-123")).thenReturn(premiumUser); when(productService.findById("prod-456")).thenReturn(saleProduct); // ==================== WHEN ==================== // 执行被测操作 PriceCalculationResult result = priceCalculator.calculate( "user-123", "prod-456", 2 // 购买2件 ); // ==================== THEN ==================== // 验证结果 assertAll("价格计算验证", // 验证业务值 () -> assertEquals(new BigDecimal("144.00"), result.getFinalPrice(), "Premium用户应该享受折上折: 100*0.8*0.9*2"), // 验证折扣明细 () -> assertEquals(2, result.getAppliedDiscounts().size()), () -> assertTrue(result.getAppliedDiscounts().contains("SALE_20%")), () -> assertTrue(result.getAppliedDiscounts().contains("PREMIUM_10%")), // 验证行为 () -> verify(userService).findById("user-123"), () -> verify(productService).findById("prod-456") ); } ``` ### 6.3 测试文档注释规范 ```java /** * 测试场景: 高并发下的库存扣减 * * 业务规则: * - 每个商品有固定库存 * - 多用户同时购买时,不应该超卖 * - 库存为0时,后续购买请求应该失败 * * 测试策略: * - 使用15个并发线程模拟15个用户同时购买 * - 商品初始库存为10 * - 验证最终只有10个购买成功 * * 相关需求: REQ-001, REQ-045 * 关联缺陷: BUG-2024-1234 * * @see InventoryService#deductStock(String, int) * @since 1.2.0 */ @Test void shouldPreventOverselling_whenConcurrentPurchases() { // ... 测试实现 } ``` ### 6.4 检查清单 - [ ] 测试名是否描述了行为和条件? - [ ] 是否使用了should_when格式? - [ ] 是否包含Given-When-Then结构注释? - [ ] 复杂测试是否有场景说明注释? - [ ] 是否引用了相关需求和缺陷? - [ ] 是否使用了assertAll组合相关断言? --- ## 7. Mock使用准则 ### 7.1 Mock决策树 ``` 是否使用Mock? ├── 是: 外部依赖(数据库、API、消息队列) ├── 是: 不可控组件(随机数、时间、网络) ├── 是: 尚未实现的依赖 ├── 是: 测试需要特定异常场景 ├── 否: 业务逻辑本身(不应该Mock) ├── 否: 简单值对象 └── 否: 可以实际初始化的轻量对象 ``` ### 7.2 Mock最佳实践 ```java @ExtendWith(MockitoExtension.class) class OrderServiceTest { @Mock private PaymentGateway paymentGateway; // ✅ 外部服务 @Mock private InventoryService inventoryService; // ✅ 复杂依赖 @InjectMocks private OrderService orderService; // 被测对象,不Mock // ❌ 不应该Mock // @Mock // private PriceCalculator priceCalculator; // 核心业务逻辑 @Test void shouldCompleteOrder_whenPaymentSucceeds() { // Given - 配置外部依赖 when(paymentGateway.charge(any(PaymentRequest.class))) .thenReturn(PaymentResult.success("txn-123")); when(inventoryService.reserve(anyString(), anyInt())) .thenReturn(InventoryResult.success()); // When - 执行被测业务 OrderResult result = orderService.placeOrder(createValidOrder()); // Then - 验证业务结果和交互 assertEquals(OrderStatus.COMPLETED, result.getStatus()); verify(paymentGateway).charge(argThat(req -> req.getAmount().compareTo(new BigDecimal("100.00")) == 0 )); verify(inventoryService).reserve("SKU-001", 2); } } ``` ### 7.3 Mock反模式 ```java // ❌ 反模式1: Mock被测对象本身 @Test void testOrderService() { OrderService mockService = mock(OrderService.class); when(mockService.placeOrder(any())).thenReturn(mockResult); // 测试的是Mock,不是真实逻辑 assertNotNull(mockService.placeOrder(new Order())); } // ❌ 反模式2: 过度使用ArgumentCaptor @Test void testWithExcessiveCaptors() { ArgumentCaptor captor1 = ArgumentCaptor.forClass(PaymentRequest.class); ArgumentCaptor captor2 = ArgumentCaptor.forClass(String.class); ArgumentCaptor captor3 = ArgumentCaptor.forClass(Integer.class); // ... 测试代码 ... // 过度验证实现细节,而非业务结果 verify(service).method(captor1.capture(), captor2.capture(), captor3.capture()); assertEquals("value1", captor1.getValue().getField1()); assertEquals("value2", captor2.getValue()); assertEquals(42, captor3.getValue()); } // ❌ 反模式3: Mock Repository层(应该是集成测试) @Test void testWithMockedRepository() { when(userRepository.findById("123")).thenReturn(Optional.of(mockUser)); // 测试的是Mock返回的数据,不是真实数据库交互 User user = userService.findById("123"); assertEquals(mockUser.getName(), user.getName()); } // ❌ 反模式4: 不验证Mock交互 @Test void testWithoutVerification() { when(paymentGateway.charge(any())).thenReturn(success()); orderService.placeOrder(order); // 缺少verify,不知道paymentGateway是否真的被调用 } // ❌ 反模式5: 使用Mockito的一般匹配器测试具体值 @Test void testWithGeneralMatchers() { // 错误: 使用any()代替具体值 when(service.calculate(any(), any())).thenReturn(100); // 无法验证是否正确传递了参数 assertEquals(100, service.calculate(5, 10)); // 实际应该验证5*10=50 assertEquals(100, service.calculate(999, 999)); // 错误值也返回100 } ``` ### 7.4 Mock检查清单 - [ ] 只Mock外部依赖,不Mock核心业务逻辑 - [ ] 配置Mock时使用具体的参数匹配 - [ ] 验证Mock的交互(verify) - [ ] 使用ArgumentCaptor时保持简洁 - [ ] Repository层使用真实数据库(集成测试) - [ ] 考虑使用@Spy部分Mock - [ ] 使用BDDMockito(given/willReturn/then/should)提升可读性 --- ## 8. 断言最佳实践 ### 8.1 断言金字塔 ``` /\ / \ / 单 \ / 个 \ / 断言 \ ← 简单场景 /--------\ / 多个 \ / 断言 \ ← 常见场景 /--------------\ / 组合断言 \ / (assertAll) \ ← 复杂场景 /--------------------\ / 自定义断言 \ ← 领域特定 /------------------------\ ``` ### 8.2 断言选择指南 | 场景 | 推荐断言 | 示例 | |------|---------|------| | 相等性 | assertEquals | `assertEquals(expected, actual)` | | 同一性 | assertSame | `assertSame(singleton, result)` | | 非空 | assertNotNull | `assertNotNull(user.getId())` | | 布尔 | assertTrue/False | `assertTrue(user.isActive())` | | 异常 | assertThrows | `assertThrows(InvalidException.class, () -> ...)` | | 集合 | assertIterableEquals | `assertIterableEquals(expected, actual)` | | 多条件 | assertAll | `assertAll("user", () -> ..., () -> ...)` | | 超时 | assertTimeout | `assertTimeout(Duration.ofSeconds(1), () -> ...)` | | 浮点数 | assertEquals(delta) | `assertEquals(3.14, result, 0.01)` | ### 8.3 高质量断言示例 ```java @Test void shouldCreateUser_withAllFieldsProperlySet() { User user = userService.createUser(new CreateUserRequest( "john.doe@example.com", "John Doe", LocalDate.of(1990, 5, 15) )); // ✅ 组合断言 - 一次性验证所有相关属性 assertAll("用户创建验证", // 验证ID生成 () -> assertNotNull(user.getId(), "用户ID应该被生成"), () -> assertTrue(user.getId().startsWith("USR"), "ID应该以USR开头"), // 验证基本字段 () -> assertEquals("john.doe@example.com", user.getEmail()), () -> assertEquals("John Doe", user.getName()), // 验证派生字段 () -> assertEquals(34, user.getAge(), "年龄应该根据生日计算"), // 验证默认值 () -> assertEquals(UserStatus.PENDING, user.getStatus()), () -> assertNotNull(user.getCreatedAt()), () -> assertTrue(user.getCreatedAt().isBefore(LocalDateTime.now().plusSeconds(1))), // 验证关联数据 () -> assertNotNull(user.getProfile()), () -> assertTrue(user.getRoles().contains(Role.USER)) ); } @Test void shouldThrowValidationException_whenEmailIsMalformed() { InvalidRequestException exception = assertThrows( InvalidRequestException.class, () -> userService.createUser(new CreateUserRequest( "invalid-email", "John Doe", null )), "应该抛出验证异常" ); // ✅ 验证异常详情 assertAll("异常验证", () -> assertEquals("VALIDATION_ERROR", exception.getErrorCode()), () -> assertTrue(exception.getMessage().contains("email")), () -> assertEquals(1, exception.getFieldErrors().size()), () -> assertEquals("email", exception.getFieldErrors().get(0).getField()) ); } @Test void shouldReturnPagedResults_withCorrectPaginationMetadata() { Page page = userService.findAll(PageRequest.of(2, 10)); // 第3页,每页10条 // ✅ 验证复杂对象的所有方面 assertAll("分页结果验证", // 内容验证 () -> assertEquals(10, page.getContent().size(), "应该返回10条记录"), () -> assertTrue(page.getContent().stream().allMatch(u -> u.getId() != null)), // 分页元数据验证 () -> assertEquals(2, page.getNumber(), "当前页码"), () -> assertEquals(10, page.getSize(), "每页大小"), () -> assertEquals(10, page.getNumberOfElements(), "当前页元素数"), // 导航验证 () -> assertTrue(page.hasPrevious(), "应该有上一页"), () -> assertTrue(page.hasNext(), "应该有下一页"), () -> assertEquals(5, page.getTotalPages(), "总页数"), () -> assertEquals(47, page.getTotalElements(), "总元素数"), // 排序验证 () -> assertTrue(page.getSort().isSorted()), () -> assertEquals("createdAt", page.getSort().iterator().next().getProperty()) ); } ``` ### 8.4 断言反模式 ```java // ❌ 反模式1: 模糊消息 assertTrue(user.isValid(), "验证失败"); // 消息无用 assertEquals(expected, actual); // 无消息,失败时难调试 // ✅ 正确 assertTrue(user.isValid(), "用户应该有效,但实际状态: " + user.getStatus()); assertEquals(expected, actual, "订单总额应该匹配,订单ID: " + orderId); // ❌ 反模式2: 多个独立assert @Test void testUser() { assertNotNull(user); assertEquals("John", user.getName()); assertTrue(user.isActive()); // 第一个失败,后面的不会执行,不知道还有多少问题 } // ✅ 正确 - 使用assertAll @Test void testUser() { assertAll("用户验证", () -> assertNotNull(user), () -> assertEquals("John", user.getName()), () -> assertTrue(user.isActive()) ); } // ❌ 反模式3: 在测试中try-catch @Test void testWithTryCatch() { try { service.doSomething(); fail("应该抛出异常"); // 容易忘记 } catch (Exception e) { assertEquals("error", e.getMessage()); } } // ✅ 正确 - 使用assertThrows @Test void testWithAssertThrows() { Exception e = assertThrows(Exception.class, () -> service.doSomething()); assertEquals("error", e.getMessage()); } // ❌ 反模式4: 浮点数精确比较 assertEquals(0.1 + 0.2, result); // 可能失败: 0.30000000000000004 // ✅ 正确 - 使用delta assertEquals(0.3, result, 0.001); ``` ### 8.5 断言检查清单 - [ ] 断言消息是否清晰描述了期望? - [ ] 是否使用assertAll组合相关断言? - [ ] 异常测试是否使用了assertThrows? - [ ] 浮点数比较是否使用了delta? - [ ] 集合比较是否使用了适当的断言? - [ ] 是否验证了所有重要的输出字段? - [ ] 断言顺序是否合理(先验证前提条件)? --- ## 9. 边界条件系统化方法 ### 9.1 边界值分析矩阵 ``` 输入类型 边界值 测试用例 ───────────────────────────────────────────────────────── 数值范围 最小值-1, 最小值, -1, 0, 1, 99, 100, 101 最小值+1, 最大值-1, (范围0-100) 最大值, 最大值+1 字符串长度 空串, 1字符, "", "a", "ab", 最大长度-1, repeat("a", 254), 最大长度, 最大长度+1 repeat("a", 255), repeat("a", 256) 集合/数组 空, 1元素, [], [1], [1,2], 最大容量-1, size=999, size=1000, 最大容量, 最大容量+1 size=1001 日期时间 最小日期, 最大日期, 1970-01-01, 闰年2月29日, 2038-01-19 (Y2K38) 时区边界 2024-02-29 枚举值 第一个, 最后一个, ENUM_FIRST, 随机中间值 ENUM_LAST, random choice 布尔/状态 true, false, isActive=true, 无效状态值(如需要) isActive=false ``` ### 9.2 系统化边界测试实现 ```java /** * 边界测试策略实现 */ class BoundaryTestStrategy { /** * 1. 数值边界测试 */ @ParameterizedTest @CsvSource({ "-1, false", // 低于最小值 "0, true", // 最小值 "1, true", // 最小值+1 "99, true", // 最大值-1 "100, true", // 最大值 "101, false", // 超过最大值 "2147483647, false", // Integer.MAX_VALUE (溢出风险) }) void shouldValidateRange_forParticipantCount(int count, boolean expectedValid) { ActivityRequest request = ActivityRequest.builder() .participantCount(count) .build(); Set> violations = validator.validate(request); boolean hasViolation = violations.stream() .anyMatch(v -> v.getPropertyPath().toString().equals("participantCount")); assertEquals(!expectedValid, hasViolation, "参与者数量 " + count + " 应该" + (expectedValid ? "有效" : "无效")); } /** * 2. 字符串边界测试 */ @ParameterizedTest @ValueSource(strings = { "", // 空字符串 "a", // 1字符 "ab", // 2字符 "a".repeat(254), // 最大长度-1 "a".repeat(255), // 最大长度 "a".repeat(256), // 最大长度+1 " ", // 仅空白字符 "test@example.com", // 有效格式 "test", // 无效格式(无@) "test@", // 无效格式(无domain) "@example.com", // 无效格式(无local) }) void shouldValidateEmail_forVariousInputs(String email) { UserRequest request = UserRequest.builder() .email(email) .build(); boolean isValid = email != null && email.length() >= 5 && email.length() <= 255 && email.contains("@") && email.indexOf("@") > 0 && email.indexOf("@") < email.length() - 1; Set> violations = validator.validate(request); assertEquals(!isValid, !violations.isEmpty(), "邮箱 '" + (email == null ? "null" : email.substring(0, Math.min(email.length(), 20))) + "...' 验证结果应该匹配预期"); } /** * 3. 集合边界测试 */ @Test void shouldHandleCollectionBoundaries() { // 空集合 assertThrows(IllegalArgumentException.class, () -> batchProcessor.processBatch(Collections.emptyList()) ); // 单元素 List singleItem = Collections.singletonList(new Item("1")); BatchResult result1 = batchProcessor.processBatch(singleItem); assertEquals(1, result1.getProcessedCount()); // 最大容量 List maxItems = IntStream.range(0, 1000) .mapToObj(i -> new Item(String.valueOf(i))) .collect(Collectors.toList()); BatchResult resultMax = batchProcessor.processBatch(maxItems); assertEquals(1000, resultMax.getProcessedCount()); // 超过最大容量 List overMaxItems = IntStream.range(0, 1001) .mapToObj(i -> new Item(String.valueOf(i))) .collect(Collectors.toList()); assertThrows(BatchSizeExceededException.class, () -> batchProcessor.processBatch(overMaxItems) ); } /** * 4. 时间边界测试 */ @ParameterizedTest @CsvSource({ "1970-01-01T00:00:00Z, true", // Epoch开始 "2024-02-29T12:00:00Z, true", // 闰年2月29日 "2023-02-29T12:00:00Z, false", // 非闰年2月29日(无效) "2038-01-19T03:14:07Z, true", // Y2K38边界前 "2038-01-19T03:14:08Z, false", // Y2K38边界后(32位时间戳溢出) "2100-01-01T00:00:00Z, true", // 远期日期 }) void shouldValidateDateTime_forBoundaryValues( String dateTimeStr, boolean expectedValid) { try { Instant instant = Instant.parse(dateTimeStr); boolean isValid = instant.isAfter(Instant.EPOCH) && instant.isBefore(Instant.parse("2100-01-01T00:00:00Z")); assertEquals(expectedValid, isValid, "日期时间 " + dateTimeStr + " 验证结果应该为 " + expectedValid); } catch (DateTimeParseException e) { assertFalse(expectedValid, "无效的日期时间格式应该验证失败"); } } /** * 5. null和特殊值测试 */ @ParameterizedTest @NullAndEmptySource @ValueSource(strings = {" ", "\t", "\n", "null", "NULL", "undefined"}) void shouldHandleSpecialStringValues(String input) { // 测试系统对各种特殊字符串值的处理 String sanitized = sanitizer.sanitize(input); assertNotNull(sanitized, "不应该返回null"); assertFalse(sanitized.equals("null"), "不应该保留字符串'null'"); assertFalse(sanitized.equals("undefined"), "不应该保留字符串'undefined'"); } } /** * 边界测试数据生成器 */ class BoundaryTestDataGenerator { static Stream provideIntegerBoundaries(int min, int max) { return Stream.of( Arguments.of(min - 1, false, "below-minimum"), Arguments.of(min, true, "minimum"), Arguments.of(min + 1, true, "minimum+1"), Arguments.of((min + max) / 2, true, "middle"), Arguments.of(max - 1, true, "maximum-1"), Arguments.of(max, true, "maximum"), Arguments.of(max + 1, false, "above-maximum") ); } static Stream provideStringBoundaries(int maxLength) { return Stream.of( "", // 空 "a", // 1字符 "a".repeat(maxLength / 2), // 中等长度 "a".repeat(maxLength - 1), // 最大-1 "a".repeat(maxLength), // 最大 "a".repeat(maxLength + 1), // 最大+1 " ", // 仅空白 "\t\n\r", // 控制字符 "", // XSS尝试 "DROP TABLE users;--", // SQL注入尝试 "日本語テスト", // Unicode "🔥🎉💯", // Emoji new String(new char[10000]).replace('\0', 'a') // 超长 ); } } ``` ### 9.3 边界条件检查清单 - [ ] 是否测试了最小值和最大值? - [ ] 是否测试了最小值-1和最大值+1? - [ ] 是否测试了空值(null, 空串, 空集合)? - [ ] 是否测试了0值(除零风险)? - [ ] 是否测试了负数(如果适用)? - [ ] 是否测试了极大值(溢出风险)? - [ ] 是否测试了特殊字符和Unicode? - [ ] 是否测试了日期边界(闰年、时区)? - [ ] 是否使用了@ParameterizedTest系统化测试? - [ ] 是否测试了并发情况下的边界? --- ## 10. 持续改进检查表 ### 10.1 定期审查计划 ``` 频率 审查内容 负责人 ───────────────────────────────────────────────────────── 每日 新失败的测试分析 开发团队 每周 测试运行时间趋势分析 技术负责人 每月 测试质量评分和覆盖率审查 QA团队 每季度 全量测试审查和重构计划 架构师团队 每年 测试策略评估和工具升级 技术委员会 ``` ### 10.2 测试健康度指标 ```java /** * 测试健康度评估脚本 */ class TestHealthMetrics { /** * 1. 计算测试脆弱性指数 */ void calculateTestFragilityIndex() { Map metrics = new HashMap<>(); // 不稳定测试比例 int flakyTests = countFlakyTests(); int totalTests = countTotalTests(); double flakyRatio = (double) flakyTests / totalTests; // 平均修复时间 double avgFixTime = calculateAverageFixTime(); // 失败频率 double failureFrequency = calculateFailureFrequency(); // 脆弱性指数 = 不稳定比例 * 0.4 + 修复时间因子 * 0.3 + 失败频率 * 0.3 double fragilityIndex = flakyRatio * 0.4 + (avgFixTime / 24) * 0.3 + failureFrequency * 0.3; System.out.println("测试脆弱性指数: " + String.format("%.2f", fragilityIndex)); System.out.println(" - 不稳定测试: " + flakyTests + "/" + totalTests); System.out.println(" - 平均修复时间: " + avgFixTime + "小时"); System.out.println(" - 失败频率: " + String.format("%.2f", failureFrequency)); } /** * 2. 测试ROI计算 */ void calculateTestROI() { // 投入 double developmentTime = 120; // 小时 double maintenanceTimePerMonth = 8; // 小时 double infrastructureCost = 500; // 月度基础设施成本 // 产出 int bugsPrevented = 15; // 月度预防的缺陷数 double avgBugCost = 20; // 平均缺陷修复成本(小时) int regressionBugsPrevented = 5; double totalInvestment = developmentTime + (maintenanceTimePerMonth * 12) + (infrastructureCost * 12 / 100); // 折算为小时 double totalReturn = (bugsPrevented * avgBugCost * 12) + (regressionBugsPrevented * avgBugCost * 2 * 12); // 回归缺陷成本更高 double roi = (totalReturn - totalInvestment) / totalInvestment * 100; System.out.println("测试ROI: " + String.format("%.1f%%", roi)); } } ``` ### 10.3 改进行动清单 #### 立即行动(本周) - [ ] 识别并标记所有不稳定测试 - [ ] 删除或重构虚假断言测试 - [ ] 修复最近的测试失败 - [ ] 更新测试文档 #### 短期行动(本月) - [ ] 实施测试命名规范检查 - [ ] 添加边界条件测试覆盖率 - [ ] 优化慢速测试(>1秒) - [ ] 建立测试基线性能指标 - [ ] 审查Mock使用并修复过度Mock #### 中期行动(本季度) - [ ] 实施并行测试执行 - [ ] 建立测试质量评分流程 - [ ] 添加并发测试套件 - [ ] 实施测试数据工厂模式 - [ ] 建立性能测试基线 #### 长期行动(本年度) - [ ] 评估并升级测试框架 - [ ] 实施契约测试 - [ ] 建立测试可视化仪表板 - [ ] 建立测试驱动开发文化 - [ ] 实施AI测试生成质量门禁 ### 10.4 自动化质量门禁 ```yaml # .github/workflows/test-quality-gate.yml name: Test Quality Gate on: [push, pull_request] jobs: quality-gate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Run Test Quality Checks run: | # 1. 检查虚假断言 FAKE_ASSERTIONS=$(grep -r "assertTrue(true)\|assertFalse(false)" src/test --include="*.java" | wc -l) if [ $FAKE_ASSERTIONS -gt 0 ]; then echo "❌ 发现 $FAKE_ASSERTIONS 个虚假断言" exit 1 fi # 2. 检查getter/setter测试 DTO_TESTS=$(grep -r "testGetter\|testSetter" src/test --include="*.java" | wc -l) if [ $DTO_TESTS -gt 10 ]; then echo "❌ 发现 $DTO_TESTS 个疑似DTO框架测试" exit 1 fi # 3. 检查测试覆盖率 mvn jacoco:report COVERAGE=$(grep -o 'Total[^%]*%' target/site/jacoco/index.html | grep -o '[0-9]*%' | head -1) echo "当前覆盖率: $COVERAGE" # 4. 检查测试执行时间 SLOW_TESTS=$(mvn test 2>&1 | grep -c "Time elapsed.*> 1 sec") if [ $SLOW_TESTS -gt 20 ]; then echo "⚠️ 发现 $SLOW_TESTS 个慢速测试" fi echo "✅ 测试质量门禁通过" ``` ### 10.5 测试改进度量表 | 指标 | 基线 | 目标 | 当前 | 差距 | |------|------|------|------|------| | 虚假断言数量 | 50 | 0 | ? | 待评估 | | DTO框架测试数量 | 30 | 0 | ? | 待评估 | | 平均测试执行时间 | 2.5s | <1s | ? | 待评估 | | 不稳定测试比例 | 8% | <2% | ? | 待评估 | | 边界条件覆盖率 | 45% | >80% | ? | 待评估 | | 并发测试数量 | 0 | >20 | ? | 待评估 | | 性能测试覆盖 | 0% | >50% | ? | 待评估 | | 测试命名规范率 | 60% | >95% | ? | 待评估 | --- ## 附录A: 快速参考卡 ### 测试反模式速查表 | 反模式 | 检测方法 | 修复建议 | |--------|----------|----------| | 虚假断言 | `grep -r "assertTrue(true)"` | 删除或添加有意义的验证 | | 测试框架 | `grep -r "testGetter\|testSetter"` | 删除DTO/Entity的框架测试 | | 过度Mock | 检查Mock核心业务类 | 使用真实对象或Spy | | 硬编码 | 查找魔法数字 | 提取为命名常量 | | 测试依赖 | 检查@Order注解 | 移除依赖,独立准备数据 | | 缺少边界 | 检查参数化测试 | 添加@ParameterizedTest | | 慢测试 | 执行时间>1秒 | 优化或使用@Tag("slow") | | 无文档 | 检查注释和命名 | 添加Given-When-Then | ### 代码审查检查清单(复制到PR模板) ```markdown ## 测试代码审查检查单 ### 质量 - [ ] 测试验证了有意义的业务逻辑(非框架代码) - [ ] 断言具体且有意义(非assertTrue(true)) - [ ] 使用了组合断言assertAll验证多个条件 - [ ] 异常测试使用了assertThrows ### 边界 - [ ] 测试了null输入 - [ ] 测试了空集合/字符串 - [ ] 测试了最大值/最小值 - [ ] 测试了无效/异常输入 ### 可维护性 - [ ] 测试名使用should_when格式 - [ ] 使用了Given-When-Then结构 - [ ] 无硬编码魔法值 - [ ] 重复的setup抽取为方法 ### Mock - [ ] 只Mock外部依赖 - [ ] 没有Mock核心业务逻辑 - [ ] 配置了具体的参数匹配 - [ ] 验证了重要的Mock交互 ### 并发 - [ ] 并发场景使用了CountDownLatch - [ ] 验证了不变量和最终一致性 - [ ] 考虑了竞态条件 - [ ] 测试可以并行执行 ``` --- ## 附录B: 推荐阅读 1. **《xUnit Test Patterns》** - Gerard Meszaros 2. **《Test Driven Development: By Example》** - Kent Beck 3. **《Growing Object-Oriented Software, Guided by Tests》** - Freeman & Pryce 4. **JUnit 5官方文档**: https://junit.org/junit5/docs/current/user-guide/ 5. **Mockito最佳实践**: https://site.mockito.org/ --- ## 版本历史 | 版本 | 日期 | 修改内容 | 作者 | |------|------|----------|------| | 1.0 | 2026-02-03 | 初始版本,基于1210个测试的经验总结 | AI Assistant | --- **如何使用本文档:** 1. **新开发人员**: 阅读第1、6、8章,了解基本规范 2. **代码审查**: 使用附录A的检查清单 3. **重构测试**: 参考第3、7、9章的具体方法 4. **建立规范**: 实施第10章的持续改进流程 5. **团队培训**: 分享第1章的AI陷阱识别,避免生成低质量测试 --- *本文档基于蚊子项目真实测试代码的经验教训编制,旨在建立高质量、可维护的测试文化。*