Files
wenzi/docs/TESTING_BEST_PRACTICES.md
Your Name 91a0b77f7a test(cache): 修复CacheConfigTest边界值测试
- 修改 shouldVerifyCacheManager_withMaximumIntegerTtl 为 shouldVerifyCacheManager_withMaximumAllowedTtl
- 使用正确的最大TTL值(10080分钟,7天)而不是 Integer.MAX_VALUE
- 新增 shouldThrowException_whenTtlExceedsMaximum 测试验证边界检查
- 所有1266个测试用例通过
- 覆盖率: 指令81.89%, 行88.48%, 分支51.55%

docs: 添加项目状态报告
- 生成 PROJECT_STATUS_REPORT.md 详细记录项目当前状态
- 包含质量指标、已完成功能、待办事项和技术债务
2026-03-02 13:31:54 +08:00

50 KiB
Raw Permalink Blame History

测试最佳实践检查清单

基于蚊子项目1210+测试的真实经验总结

本文档旨在帮助识别和规避AI生成测试中常见的陷阱提升测试质量和可维护性。

📋 目录

  1. AI测试陷阱识别清单
  2. 测试质量评估矩阵
  3. 可维护性检查清单
  4. 性能测试最佳实践
  5. 并发测试策略
  6. 测试命名与文档规范
  7. Mock使用准则
  8. 断言最佳实践
  9. 边界条件系统化方法
  10. 持续改进检查表

1. AI测试陷阱识别清单

1.1 过度测试框架本身 🚫

问题描述

AI常生成测试Lombok生成的getter/setter、构造函数或框架代码的测试这些测试毫无意义。

错误示例

@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()); // 测试简单赋值
}

正确示例

@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());
}

检查方法

  • 是否测试了框架生成的代码?
  • 是否测试了无业务逻辑的简单赋值?
  • 是否测试了编译器会保证的行为?

自动化规则

# 查找可疑的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 虚假断言 🚫

问题描述

断言永远不会失败,或只验证显而易见的事情,无法真正验证业务逻辑。

错误示例

@Test
void testCreateObject() {
    Object obj = new Object();
    assertNotNull(obj); // 永远不会失败
}

@Test
void testTrueIsTrue() {
    assertTrue(true); // 无意义断言
}

@Test
void testServiceCalled() {
    service.doSomething();
    verify(service).doSomething(); // 只验证方法被调用
}

正确示例

@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()); // 验证副作用
}

检查方法

  • 断言是否可能失败?
  • 是否验证了具体的业务值?
  • 是否验证了状态变化?

自动化规则

# 查找虚假断言
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 自动化评估脚本

#!/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 硬编码值问题

问题描述

测试中使用魔法数字和字符串,导致测试脆弱且难以理解。

错误示例

@Test
void testCalculation() {
    assertEquals(1000, service.calculate(500, 2)); // 魔法数字
    assertEquals("ERROR_001", exception.getCode()); // 无意义错误码
}

正确示例

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和断言逻辑难以维护。

错误示例

@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());
    // ... 同样的重复
}

正确示例

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 测试间依赖问题

问题描述

测试之间存在隐式依赖,执行顺序影响结果,难以并行运行。

错误示例

@Test
@Order(1) // 强制顺序本身就是问题信号
void testCreate() {
    userService.createUser("test");
}

@Test
@Order(2) // 依赖testCreate
void testRead() {
    User user = userService.findByName("test"); // 依赖前面的测试数据
    assertNotNull(user);
}

正确示例

@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 性能测试类型

/**
 * 1. 基线测试 - 记录正常性能指标
 */
@Test
void shouldCompleteWithinBaseline_whenProcessingStandardLoad() {
    // Given
    List<Data> 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<Long> 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 性能反模式

// ❌ 在单元测试中测试性能
@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 并发测试基本原则

/**
 * 并发测试黄金法则:
 * 1. 不要依赖Thread.sleep() - 使用CountDownLatch/CyclicBarrier
 * 2. 不要假设执行顺序 - 使用同步原语控制
 * 3. 测试竞争条件,而不仅仅是并发执行
 * 4. 验证最终一致性和不变量
 */

5.2 常见并发测试模式

/**
 * 模式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 不推荐

优秀命名示例

// 业务场景清晰
@Test
void shouldSendNotification_whenUserCompletesMilestone()

// 边界条件明确
@Test
void shouldRejectRequest_whenEmailExceeds255Characters()

// 异常场景具体
@Test
void shouldThrowDuplicateKeyException_whenRegisteringExistingEmail()

// 状态转换明确
@Test
void shouldTransitionFromPendingToActive_whenPaymentConfirmed()

// 并发场景清晰
@Test
void shouldMaintainConsistency_whenConcurrentUpdatesToSameEntity()

糟糕命名示例

@Test
void test1() // 无意义

@Test
void testUser() // 过于笼统

@Test
void testRegisterUserSuccess() // 没有when部分

@Test
void test() // 完全无信息

6.2 Given-When-Then结构

@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 测试文档注释规范

/**
 * 测试场景: 高并发下的库存扣减
 * 
 * 业务规则:
 * - 每个商品有固定库存
 * - 多用户同时购买时,不应该超卖
 * - 库存为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最佳实践

@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反模式

// ❌ 反模式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<PaymentRequest> captor1 = ArgumentCaptor.forClass(PaymentRequest.class);
    ArgumentCaptor<String> captor2 = ArgumentCaptor.forClass(String.class);
    ArgumentCaptor<Integer> 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
  • 使用BDDMockitogiven/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 高质量断言示例

@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<User> 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 断言反模式

// ❌ 反模式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 系统化边界测试实现

/**
 * 边界测试策略实现
 */
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<ConstraintViolation<ActivityRequest>> 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<ConstraintViolation<UserRequest>> 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<Item> singleItem = Collections.singletonList(new Item("1"));
        BatchResult result1 = batchProcessor.processBatch(singleItem);
        assertEquals(1, result1.getProcessedCount());
        
        // 最大容量
        List<Item> 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<Item> 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<Arguments> 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<String> 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",                              // 控制字符
        "<script>alert('xss')</script>",     // 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 测试健康度指标

/**
 * 测试健康度评估脚本
 */
class TestHealthMetrics {
    
    /**
     * 1. 计算测试脆弱性指数
     */
    void calculateTestFragilityIndex() {
        Map<String, Integer> 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 自动化质量门禁

# .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模板

## 测试代码审查检查单

### 质量
- [ ] 测试验证了有意义的业务逻辑(非框架代码)
- [ ] 断言具体且有意义非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陷阱识别避免生成低质量测试

本文档基于蚊子项目真实测试代码的经验教训编制,旨在建立高质量、可维护的测试文化。