🧪 单元测试用例标准模板与覆盖率要求

基于 OpenClaw + Claude Code 的端到端研发自动化系统 · 质量保障体系核心规范

📅 版本:v2026.03 | 更新日期:2026 年 3 月 13 日 | 强制生效

目录导航

📊 第一章:覆盖率标准与要求

🎯 核心目标:建立统一的单元测试质量标准,确保代码可靠性、可维护性和生产环境稳定性。所有项目必须严格遵守本规范要求。

1.1 覆盖率指标定义

代码覆盖率是衡量测试完整性的关键指标,本系统采用以下四种覆盖率度量标准:

≥95%
行覆盖率 (Line Coverage)
被测试执行到的代码行数占总行数的比例。核心业务模块必须达到 95% 以上。
≥90%
分支覆盖率 (Branch Coverage)
被测试执行到的分支(if/else、switch case 等)占总分支数的比例。
≥95%
函数覆盖率 (Function Coverage)
被测试调用到的函数占函数总数的比例。所有公开方法必须被测试覆盖。
≥85%
条件覆盖率 (Condition Coverage)
布尔表达式中每个子条件都取到真和假的测试覆盖比例。

1.2 分级覆盖率要求

模块类型 行覆盖率 分支覆盖率 函数覆盖率 优先级 验收标准
核心业务逻辑
支付、交易、认证等
≥98% ≥95% 100% P0 - 关键 强制阻断
重要业务模块
用户管理、订单处理等
≥95% ≥90% ≥98% P1 - 高 强制阻断
一般功能模块
查询、统计、报表等
≥90% ≥85% ≥95% P2 - 中 警告提示
辅助工具类
Util、Helper、Converter 等
≥85% ≥80% ≥90% P3 - 低 建议优化
DTO/VO/Entity
纯数据对象
≥60% N/A ≥80% P4 - 可选 豁免申请
⚠️ 质量门禁规则:
  • CI/CD 阻断:任何模块的覆盖率低于最低要求,CI 流水线将自动失败,禁止合并代码
  • 核心模块零容忍:支付、安全、认证等核心模块覆盖率未达标,直接驳回 PR
  • 新增代码强制覆盖:新增代码行的覆盖率必须 ≥95%,否则无法通过代码审查
  • 历史债务限期整改:存量代码覆盖率不足的,需在 3 个迭代内完成补充

1.3 豁免与例外管理

📋 覆盖率豁免申请条件

以下情况可申请覆盖率豁免,但必须经过 Tech Lead 和安全团队双重审批:

  • 生成的样板代码(Getter/Setter、toString 等)
  • 仅包含常量定义的类
  • 自动生成的 API 客户端代码
  • 第三方库的适配层代码
  • 因技术限制无法测试的代码(需提供证明)
  • 已计划重构且不影响功能的遗留代码
// 示例:豁免申请的代码注释标记
// @CoverageExemption reason: "Auto-generated by OpenAPI Generator"
// @CoverageExemption reason: "Simple DTO with no business logic"
// @CoverageExemption reason: "Legacy code scheduled for refactoring in Q2-2026"

🏗️ 第二章:AAA 测试模式详解

本系统强制采用 AAA (Arrange-Act-Assert) 模式编写所有单元测试,确保测试代码的可读性、一致性和可维护性。

1
Arrange (准备)
设置测试所需的前置条件,包括:
  • 创建测试对象实例
  • 准备输入数据和预期结果
  • 配置 Mock 对象和 Stub
  • 初始化测试环境和依赖
  • 设置测试数据状态
2
Act (执行)
执行被测试的目标操作,包括:
  • 调用被测方法或函数
  • 传入准备好的参数
  • 捕获返回值或异常
  • 记录执行结果
  • 保持 Act 部分简洁单一
3
Assert (断言)
验证执行结果是否符合预期,包括:
  • 验证返回值正确性
  • 检查对象状态变化
  • 确认副作用是否发生
  • 验证 Mock 对象交互
  • 多个断言时按重要性排序

2.1 AAA 模式完整示例(Java)

package com.example.user.service;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import com.example.user.entity.User;
import com.example.user.repository.UserRepository;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void testRegisterUser_Success() {
        // ========== Arrange (准备) ==========
        // 1. 准备测试数据
        String username = "test_user";
        String email = "test@example.com";
        String rawPassword = "SecurePass123!";
        
        // 2. 创建期望返回的用户对象
        User expectedUser = new User();
        expectedUser.setId(1L);
        expectedUser.setUsername(username);
        expectedUser.setEmail(email);
        
        // 3. 配置 Mock 行为
        when(userRepository.existsByUsername(username)).thenReturn(false);
        when(userRepository.existsByEmail(email)).thenReturn(false);
        when(userRepository.save(any(User.class))).thenReturn(expectedUser);

        // ========== Act (执行) ==========
        // 调用被测方法(只执行一个操作)
        User actualUser = userService.registerUser(username, email, rawPassword);

        // ========== Assert (断言) ==========
        // 1. 验证返回值
        assertNotNull(actualUser, "返回的用户对象不应为空");
        assertEquals(expectedUser.getId(), actualUser.getId());
        assertEquals(username, actualUser.getUsername());
        assertEquals(email, actualUser.getEmail());
        
        // 2. 验证 Mock 交互
        verify(userRepository).existsByUsername(username);
        verify(userRepository).existsByEmail(email);
        verify(userRepository).save(any(User.class));
        
        // 3. 验证未被调用的方法(确保没有多余操作)
        verifyNoMoreInteractions(userRepository);
    }

    @Test
    void testRegisterUser_UsernameAlreadyExists() {
        // ========== Arrange (准备) ==========
        String username = "existing_user";
        String email = "new@example.com";
        String password = "password123";
        
        // 配置 Mock:用户名已存在
        when(userRepository.existsByUsername(username)).thenReturn(true);

        // ========== Act & Assert (执行并断言) ==========
        // 对于异常场景,使用 assertThrows
        IllegalArgumentException exception = assertThrows(
            IllegalArgumentException.class,
            () -> userService.registerUser(username, email, password),
            "用户名已存在时应抛出 IllegalArgumentException"
        );
        
        // 验证异常消息
        assertEquals("用户名已存在:" + username, exception.getMessage());
        
        // 验证 save 方法未被调用
        verify(userRepository, never()).save(any(User.class));
    }
}

2.2 AAA 模式完整示例(Python)

import pytest
from unittest.mock import Mock, patch, call
from user_service import UserService
from exceptions import UserAlreadyExistsError, InvalidEmailError


class TestUserService:
    """UserService 单元测试类"""

    @pytest.fixture
    def mock_user_repository(self):
        """创建 Mock 仓库 fixture"""
        return Mock()

    @pytest.fixture
    def user_service(self, mock_user_repository):
        """创建被测服务 fixture"""
        return UserService(mock_user_repository)

    def test_register_user_success(self, user_service, mock_user_repository):
        """测试用户注册成功场景"""
        
        # ========== Arrange (准备) ==========
        # 1. 准备测试数据
        username = "test_user"
        email = "test@example.com"
        password = "SecurePass123!"
        
        # 2. 创建期望返回的用户对象
        expected_user = Mock(
            id=1,
            username=username,
            email=email
        )
        
        # 3. 配置 Mock 行为
        mock_user_repository.exists_by_username.return_value = False
        mock_user_repository.exists_by_email.return_value = False
        mock_user_repository.save.return_value = expected_user

        # ========== Act (执行) ==========
        # 调用被测方法
        actual_user = user_service.register_user(username, email, password)

        # ========== Assert (断言) ==========
        # 1. 验证返回值
        assert actual_user is not None, "返回的用户对象不应为空"
        assert actual_user.id == expected_user.id
        assert actual_user.username == username
        assert actual_user.email == email
        
        # 2. 验证 Mock 交互
        mock_user_repository.exists_by_username.assert_called_once_with(username)
        mock_user_repository.exists_by_email.assert_called_once_with(email)
        mock_user_repository.save.assert_called_once()
        
        # 3. 验证保存的用户数据正确
        saved_user = mock_user_repository.save.call_args[0][0]
        assert saved_user.username == username
        assert saved_user.email == email
        assert saved_user.password_hash is not None

    def test_register_user_email_already_exists(self, user_service, mock_user_repository):
        """测试邮箱已存在的异常场景"""
        
        # ========== Arrange (准备) ==========
        username = "new_user"
        email = "existing@example.com"
        password = "password123"
        
        # 配置 Mock:邮箱已存在
        mock_user_repository.exists_by_email.return_value = True

        # ========== Act & Assert (执行并断言) ==========
        # 使用 pytest.raises 验证异常
        with pytest.raises(UserAlreadyExistsError) as exc_info:
            user_service.register_user(username, email, password)
        
        # 验证异常消息
        assert str(exc_info.value) == f"邮箱已存在:{email}"
        
        # 验证 save 方法未被调用
        mock_user_repository.save.assert_not_called()
✅ AAA 模式优势:
  • 结构清晰:三个明确阶段,一目了然
  • 易于维护:修改测试时快速定位需要调整的部分
  • 促进单一职责:每个测试只验证一个行为
  • 便于审查:审查者可以快速理解测试意图
  • AI 友好:Claude Code 可以稳定生成符合 AAA 模式的测试代码

📝 第三章:通用测试用例模板

3.1 测试用例文档模板

📋 标准测试用例模板(适用于所有语言)
================================================================================
测试用例标识符:TC_[MODULE]_[FEATURE]_[SCENARIO]_[VERSION]
================================================================================

【基本信息】
- 测试用例 ID:   TC_USER_REGISTER_SUCCESS_V1
- 所属模块:     用户管理模块 (User Management)
- 功能点:       用户注册 (User Registration)
- 测试场景:     正常注册流程 (Success Path)
- 优先级:       P0 - 核心功能
- 自动化:      ✅ 已自动化
- 最后更新:     2026-03-13
- 负责人:       [开发者姓名]

【测试目的】
验证用户使用有效信息能够成功注册新账户,系统正确创建用户记录并返回用户信息。

【前置条件】
1. 数据库连接正常
2. 用户表 (users) 为空或不存在重复的用户名/邮箱
3. 密码加密服务可用
4. 邮件服务处于 Mock 模式(不实际发送邮件)

【测试数据】
┌─────────────┬────────────────────────────────────────┐
│ 字段        │ 值                                     │
├─────────────┼────────────────────────────────────────┤
│ 用户名      | test_user_2026                         │
│ 邮箱        | test_2026@example.com                  │
│ 密码        | SecurePass123!                         │
│ 期望 ID     | 1 (或自增主键)                          │
└─────────────┴────────────────────────────────────────┘

【测试步骤】
┌──────┬─────────────────────────────────────────────────────────────────────────┐
│ 步骤 │ 操作描述                                                                │
├──────┼─────────────────────────────────────────────────────────────────────────┤
│  1   │ 创建 UserService 实例,注入 Mock 的 UserRepository                      │
│  2   │ 配置 Mock:username 不存在,email 不存在                                 │
│  3   │ 配置 Mock:save() 方法返回预期的 User 对象                              │
│  4   │ 调用 registerUser(username, email, password) 方法                       │
│  5   │ 获取返回的 User 对象                                                    │
└──────┴─────────────────────────────────────────────────────────────────────────┘

【预期结果】
┌──────┬─────────────────────────────────────────────────────────────────────────┐
│ 编号 │ 验证点                                                                  │
├──────┼─────────────────────────────────────────────────────────────────────────┤
│ ER1  │ 返回的 User 对象不为 null                                                │
│ ER2  │ User.id 等于预期值 (1)                                                   │
│ ER3  │ User.username 等于输入的用户名                                          │
│ ER4  │ User.email 等于输入的邮箱                                               │
│ ER5  │ User.passwordHash 不为空且不是明文密码                                  │
│ ER6  │ UserRepository.existsByUsername() 被调用 1 次                            │
│ ER7  │ UserRepository.existsByEmail() 被调用 1 次                               │
│ ER8  │ UserRepository.save() 被调用 1 次                                        │
└──────┴─────────────────────────────────────────────────────────────────────────┘

【测试代码位置】
- 文件路径:src/test/java/com/example/user/service/UserServiceTest.java
- 测试方法:testRegisterUser_Success()
- 所属类:  UserServiceTest

【依赖与 Mock】
┌──────────────────────┬────────────────────────────────────────────────────────┐
│ 依赖项               │ Mock 配置                                              │
├──────────────────────┼────────────────────────────────────────────────────────┤
│ UserRepository       │ existsByUsername() → false                             │
│                      │ existsByEmail() → false                                │
│                      │ save(any()) → expectedUser                             │
│ PasswordEncoder      │ encode(rawPassword) → "encoded_hash"                   │
│ EmailService         │ sendWelcomeEmail() → void (不验证)                     │
└──────────────────────┴────────────────────────────────────────────────────────┘

【边界条件与异常场景】
┌───────────────────────────────────────────────────────────────────────────────┐
│ 关联测试用例:                                                                 │
│ - TC_USER_REGISTER_USERNAME_EXISTS_V1: 用户名已存在场景                        │
│ - TC_USER_REGISTER_EMAIL_EXISTS_V1: 邮箱已存在场景                             │
│ - TC_USER_REGISTER_INVALID_EMAIL_V1: 邮箱格式无效场景                          │
│ - TC_USER_REGISTER_WEAK_PASSWORD_V1: 密码强度不足场景                          │
└───────────────────────────────────────────────────────────────────────────────┘

【执行记录】
┌──────────┬────────┬──────────────┬────────────────────────────────────────────┐
│ 执行日期 │ 结果   │ 执行环境       │ 备注                                     │
├──────────┼────────┼──────────────┼────────────────────────────────────────────┤
│ 2026-03-13 │ ✅ PASS │ Jenkins CI    │ 首次执行,覆盖率 98%                      │
│          │        │              │                                            │
└──────────┴────────┴──────────────┴────────────────────────────────────────────┘

【覆盖率统计】
- 行覆盖率:    98.5% (125/127)
- 分支覆盖率:  95.2% (40/42)
- 函数覆盖率:100%  (8/8)
- 未覆盖代码:第 45 行(日志记录)、第 78 行(异常处理兜底)

【评审记录】
┌──────────┬────────────┬───────────────────────────────────────────────────────┐
│ 评审日期 │ 评审人     │ 评审意见                                              │
├──────────┼────────────┼───────────────────────────────────────────────────────┤
│ 2026-03-13 │ 张三 (TL)  │ ✅ 通过。测试覆盖全面,断言充分,Mock 配置合理。       │
└──────────┴────────────┴───────────────────────────────────────────────────────┘

================================================================================

3.2 测试命名规范

📝 测试方法命名规则:
  • 格式:test[MethodName]_[Scenario]_[ExpectedResult]
  • 示例:testRegisterUser_Success_ReturnsUserObject
  • 异常场景:testRegisterUser_UsernameExists_ThrowsIllegalArgumentException
  • 中文项目:可使用中文命名test 用户注册 _ 成功 _ 返回用户对象

☕ 第四章:Java/JUnit 测试模板

4.1 JUnit 5 + Mockito 完整模板

package com.example.order.service;

/**
 * OrderService 单元测试类
 * 
 * @author AI Coding Agent
 * @date 2026-03-13
 * @version 1.0
 * @see OrderService
 */
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
import com.example.order.entity.Order;
import com.example.order.entity.OrderStatus;
import com.example.order.repository.OrderRepository;
import com.example.product.repository.ProductRepository;
import com.example.payment.service.PaymentService;
import java.math.BigDecimal;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

/**
 * OrderService 单元测试
 * 
 * 测试覆盖策略:
 * 1. 正常流程测试 - 验证业务主流程
 * 2. 异常流程测试 - 验证各种异常情况
 * 3. 边界条件测试 - 验证边界值处理
 * 4. 集成交互测试 - 验证依赖组件交互
 */
@DisplayName("OrderService 单元测试")
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    // ========== Mock 对象声明 ==========
    @Mock
    private OrderRepository orderRepository;

    @Mock
    private ProductRepository productRepository;

    @Mock
    private PaymentService paymentService;

    @InjectMocks
    private OrderService orderService;

    // ========== 测试夹具 (Test Fixture) ==========
    private Order testOrder;
    private Long orderId;
    private Long userId;

    @BeforeEach
    void setUp() {
        // 每个测试前的准备工作
        orderId = 1L;
        userId = 100L;
        
        testOrder = new Order();
        testOrder.setId(orderId);
        testOrder.setUserId(userId);
        testOrder.setAmount(new BigDecimal("299.00"));
        testOrder.setStatus(OrderStatus.PENDING);
    }

    @AfterEach
    void tearDown() {
        // 每个测试后的清理工作
        reset(orderRepository, productRepository, paymentService);
    }

    // ========== 正常流程测试 ==========
    
    @Nested
    @DisplayName("创建订单 - 正常流程")
    class CreateOrderSuccessTests {

        @Test
        @DisplayName("测试创建订单成功 - 返回订单对象")
        void testCreateOrder_Success_ReturnsOrderObject() {
            // ========== Arrange ==========
            Long productId = 50L;
            int quantity = 2;
            BigDecimal unitPrice = new BigDecimal("149.50");
            BigDecimal expectedAmount = new BigDecimal("299.00");
            
            // Mock 商品存在
            when(productRepository.findById(productId))
                .thenReturn(Optional.of(new Product(productId, "Test Product", unitPrice)));
            
            // Mock 保存订单
            when(orderRepository.save(any(Order.class)))
                .thenAnswer(invocation -> {
                    Order savedOrder = invocation.getArgument(0);
                    savedOrder.setId(orderId);
                    return savedOrder;
                });

            // ========== Act ==========
            Order createdOrder = orderService.createOrder(userId, productId, quantity);

            // ========== Assert ==========
            // 验证返回值
            assertNotNull(createdOrder, "创建的订单不应为空");
            assertEquals(orderId, createdOrder.getId());
            assertEquals(userId, createdOrder.getUserId());
            assertEquals(productId, createdOrder.getProductId());
            assertEquals(quantity, createdOrder.getQuantity());
            assertEquals(expectedAmount, createdOrder.getAmount());
            assertEquals(OrderStatus.PENDING, createdOrder.getStatus());
            
            // 验证交互
            verify(productRepository).findById(productId);
            verify(orderRepository).save(any(Order.class));
            verifyNoMoreInteractions(productRepository, orderRepository);
        }
    }

    // ========== 异常流程测试 ==========
    
    @Nested
    @DisplayName("创建订单 - 异常流程")
    class CreateOrderExceptionTests {

        @Test
        @DisplayName("测试商品不存在 - 抛出 ProductNotFoundException")
        void testCreateOrder_ProductNotFound_ThrowsException() {
            // ========== Arrange ==========
            Long nonExistentProductId = 999L;
            int quantity = 1;
            
            when(productRepository.findById(nonExistentProductId))
                .thenReturn(Optional.empty());

            // ========== Act & Assert ==========
            ProductNotFoundException exception = assertThrows(
                ProductNotFoundException.class,
                () -> orderService.createOrder(userId, nonExistentProductId, quantity),
                "商品不存在时应抛出 ProductNotFoundException"
            );
            
            assertEquals("商品不存在:" + nonExistentProductId, exception.getMessage());
            verify(orderRepository, never()).save(any());
        }

        @Test
        @DisplayName("测试库存不足 - 抛出 InsufficientStockException")
        void testCreateOrder_InsufficientStock_ThrowsException() {
            // ========== Arrange ==========
            Long productId = 50L;
            int requestedQuantity = 100;
            int availableStock = 10;
            
            Product product = new Product(productId, "Test Product", new BigDecimal("99.00"));
            product.setStock(availableStock);
            
            when(productRepository.findById(productId))
                .thenReturn(Optional.of(product));

            // ========== Act & Assert ==========
            InsufficientStockException exception = assertThrows(
                InsufficientStockException.class,
                () -> orderService.createOrder(userId, productId, requestedQuantity)
            );
            
            assertTrue(exception.getMessage().contains("库存不足"));
            assertTrue(exception.getMessage().contains(String.valueOf(availableStock)));
        }
    }

    // ========== 边界条件测试 ==========
    
    @Nested
    @DisplayName("创建订单 - 边界条件")
    class CreateOrderBoundaryTests {

        @Test
        @DisplayName("测试购买数量为 0 - 抛出 IllegalArgumentException")
        void testCreateOrder_ZeroQuantity_ThrowsException() {
            // ========== Arrange ==========
            Long productId = 50L;
            int zeroQuantity = 0;

            // ========== Act & Assert ==========
            IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> orderService.createOrder(userId, productId, zeroQuantity)
            );
            
            assertEquals("购买数量必须大于 0", exception.getMessage());
        }

        @Test
        @DisplayName("测试购买数量超过最大值 - 抛出 IllegalArgumentException")
        void testCreateOrder_ExcessiveQuantity_ThrowsException() {
            // ========== Arrange ==========
            Long productId = 50L;
            int excessiveQuantity = 10000;

            // ========== Act & Assert ==========
            IllegalArgumentException exception = assertThrows(
                IllegalArgumentException.class,
                () -> orderService.createOrder(userId, productId, excessiveQuantity)
            );
            
            assertTrue(exception.getMessage().contains("超过最大购买限制"));
        }
    }

    // ========== 集成交互测试 ==========
    
    @Nested
    @DisplayName("订单支付 - 集成交互")
    class OrderPaymentIntegrationTests {

        @Test
        @DisplayName("测试支付成功 - 更新订单状态")
        void testPayOrder_Success_UpdatesOrderStatus() {
            // ========== Arrange ==========
            String transactionId = "TXN_20260313_001";
            
            when(orderRepository.findById(orderId))
                .thenReturn(Optional.of(testOrder));
            
            when(paymentService.processPayment(any()))
                .thenReturn(new PaymentResult(transactionId, true, "SUCCESS"));
            
            when(orderRepository.save(any(Order.class)))
                .thenAnswer(invocation -> invocation.getArgument(0));

            // ========== Act ==========
            PaymentResult result = orderService.payOrder(orderId);

            // ========== Assert ==========
            assertTrue(result.isSuccess());
            assertEquals(transactionId, result.getTransactionId());
            
            // 验证订单状态更新为 PAID
            ArgumentCaptor orderCaptor = ArgumentCaptor.forClass(Order.class);
            verify(orderRepository).save(orderCaptor.capture());
            Order savedOrder = orderCaptor.getValue();
            assertEquals(OrderStatus.PAID, savedOrder.getStatus());
            assertEquals(transactionId, savedOrder.getTransactionId());
        }
    }
}

4.2 Maven 配置示例

<project>
    ...
    <properties>
        <junit.version>5.10.2</junit.version>
        <mockito.version>5.11.0</mockito.version>
        <jacoco.version>0.8.12</jacoco.version>
    </properties>

    <dependencies>
        <<!-- JUnit 5 -->>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
        
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>

        <<!-- Mockito -->>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-junit-jupiter</artifactId>
            <version>${mockito.version}</version>
            <scope>test</scope>
        </dependency>

        <<!-- AssertJ (流式断言) -->>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.25.3</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <<!-- JaCoCo 覆盖率插件 -->>
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>${jacoco.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>report</id>
                        <phase>test</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <rules>
                        <rule>
                            <element>BUNDLE</element>
                            <limits>
                                <limit>
                                    <counter>LINE</counter>
                                    <value>COVEREDRATIO</value>
                                    <minimum>0.95</minimum>
                                </limit>
                                <limit>
                                    <counter>BRANCH</counter>
                                    <value>COVEREDRATIO</value>
                                    <minimum>0.90</minimum>
                                </limit>
                            </limits>
                        </rule>
                    </rules>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

🐍 第五章:Python/pytest 测试模板

5.1 pytest 完整测试模板

"""
UserService 单元测试模块

作者:AI Coding Agent
日期:2026-03-13
版本:1.0
"""
import pytest
from unittest.mock import Mock, MagicMock, patch, call
from datetime import datetime, timedelta
from typing import Optional

# 导入被测模块
from src.services.user_service import UserService
from src.repositories.user_repository import UserRepository
from src.models.user import User, UserRole
from src.exceptions import (
    UserAlreadyExistsError,
    UserNotFoundError,
    InvalidCredentialsError,
    WeakPasswordError
)


class TestUserService:
    """UserService 单元测试类
    
    测试覆盖策略:
    1. 正常流程测试 - 验证业务主流程
    2. 异常流程测试 - 验证各种异常情况
    3. 边界条件测试 - 验证边界值处理
    4. 集成交互测试 - 验证依赖组件交互
    """

    # ========== Fixtures (测试夹具) ==========
    
    @pytest.fixture
    def mock_user_repo(self):
        """创建 Mock 用户仓库"""
        repo = Mock(spec=UserRepository)
        return repo

    @pytest.fixture
    def user_service(self, mock_user_repo):
        """创建 UserService 实例"""
        return UserService(mock_user_repo)

    @pytest.fixture
    def sample_user_data(self):
        """样本用户数据"""
        return {
            "username": "test_user_2026",
            "email": "test_2026@example.com",
            "password": "SecurePass123!",
            "role": UserRole.USER
        }

    @pytest.fixture
    def expected_user(self, sample_user_data):
        """期望的用户对象"""
        user = User(
            id=1,
            username=sample_user_data["username"],
            email=sample_user_data["email"],
            password_hash="hashed_password",
            role=sample_user_data["role"],
            created_at=datetime.now()
        )
        return user

    # ========== 正常流程测试 ==========
    
    class TestUserRegistration:
        """用户注册测试套件"""

        def test_register_user_success(
            self,
            user_service,
            mock_user_repo,
            sample_user_data,
            expected_user
        ):
            """测试用户注册成功场景"""
            
            # ========== Arrange ==========
            # 配置 Mock 行为
            mock_user_repo.exists_by_username.return_value = False
            mock_user_repo.exists_by_email.return_value = False
            mock_user_repo.save.return_value = expected_user

            # ========== Act ==========
            result = user_service.register_user(
                username=sample_user_data["username"],
                email=sample_user_data["email"],
                password=sample_user_data["password"]
            )

            # ========== Assert ==========
            # 验证返回值
            assert result is not None
            assert result.id == expected_user.id
            assert result.username == sample_user_data["username"]
            assert result.email == sample_user_data["email"]
            assert result.password_hash != sample_user_data["password"]  # 密码已加密
            
            # 验证 Mock 调用
            mock_user_repo.exists_by_username.assert_called_once_with(
                sample_user_data["username"]
            )
            mock_user_repo.exists_by_email.assert_called_once_with(
                sample_user_data["email"]
            )
            mock_user_repo.save.assert_called_once()

        def test_register_user_with_default_role(
            self,
            user_service,
            mock_user_repo,
            expected_user
        ):
            """测试用户使用默认角色注册"""
            
            # ========== Arrange ==========
            mock_user_repo.exists_by_username.return_value = False
            mock_user_repo.exists_by_email.return_value = False
            mock_user_repo.save.return_value = expected_user

            # ========== Act ==========
            result = user_service.register_user(
                username="new_user",
                email="new@example.com",
                password="Password123!"
            )

            # ========== Assert ==========
            assert result.role == UserRole.USER  # 默认角色
            
            # 验证保存时使用了正确的角色
            saved_user = mock_user_repo.save.call_args[0][0]
            assert saved_user.role == UserRole.USER

    # ========== 异常流程测试 ==========
    
    class TestUserRegistrationExceptions:
        """用户注册异常测试套件"""

        def test_register_user_username_exists(
            self,
            user_service,
            mock_user_repo
        ):
            """测试用户名已存在场景"""
            
            # ========== Arrange ==========
            mock_user_repo.exists_by_username.return_value = True

            # ========== Act & Assert ==========
            with pytest.raises(UserAlreadyExistsError) as exc_info:
                user_service.register_user(
                    username="existing_user",
                    email="new@example.com",
                    password="Password123!"
                )
            
            assert "用户名已存在" in str(exc_info.value)
            mock_user_repo.save.assert_not_called()

        def test_register_user_email_exists(
            self,
            user_service,
            mock_user_repo
        ):
            """测试邮箱已存在场景"""
            
            # ========== Arrange ==========
            mock_user_repo.exists_by_username.return_value = False
            mock_user_repo.exists_by_email.return_value = True

            # ========== Act & Assert ==========
            with pytest.raises(UserAlreadyExistsError) as exc_info:
                user_service.register_user(
                    username="new_user",
                    email="existing@example.com",
                    password="Password123!"
                )
            
            assert "邮箱已存在" in str(exc_info.value)

        def test_register_user_weak_password(
            self,
            user_service,
            mock_user_repo
        ):
            """测试密码强度不足场景"""
            
            # ========== Arrange ==========
            weak_passwords = [
                "123456",
                "password",
                "abcdef",
                "12345678"
            ]

            # ========== Act & Assert ==========
            for weak_pwd in weak_passwords:
                with pytest.raises(WeakPasswordError):
                    user_service.register_user(
                        username="test_user",
                        email="test@example.com",
                        password=weak_pwd
                    )

    # ========== 边界条件测试 ==========
    
    class TestUserRegistrationBoundaries:
        """用户注册边界测试套件"""

        def test_register_user_username_length_limits(
            self,
            user_service,
            mock_user_repo
        ):
            """测试用户名长度限制"""
            
            # ========== Arrange ==========
            mock_user_repo.exists_by_username.return_value = False
            mock_user_repo.exists_by_email.return_value = False

            # ========== Act & Assert ==========
            # 用户名太短
            with pytest.raises(ValueError) as exc_info:
                user_service.register_user(
                    username="ab",  # 少于 3 个字符
                    email="test@example.com",
                    password="Password123!"
                )
            assert "用户名长度" in str(exc_info.value)

            # 用户名太长
            long_username = "a" * 51  # 超过 50 个字符
            with pytest.raises(ValueError):
                user_service.register_user(
                    username=long_username,
                    email="test@example.com",
                    password="Password123!"
                )

        def test_register_user_email_format_validation(
            self,
            user_service,
            mock_user_repo
        ):
            """测试邮箱格式验证"""
            
            # ========== Arrange ==========
            invalid_emails = [
                "invalid-email",
                "@example.com",
                "user@",
                "user@.com",
                "user@example"
            ]

            # ========== Act & Assert ==========
            for invalid_email in invalid_emails:
                with pytest.raises(ValueError):
                    user_service.register_user(
                        username="test_user",
                        email=invalid_email,
                        password="Password123!"
                    )

    # ========== 参数化测试 ==========
    
    class TestUserLoginParametrized:
        """用户登录参数化测试套件"""

        @pytest.mark.parametrize(
            "email,password,expected_exception",
            [
                ("notfound@example.com", "Password123!", UserNotFoundError),
                ("user@example.com", "wrong_password", InvalidCredentialsError),
                ("", "Password123!", ValueError),
                ("user@example.com", "", ValueError),
            ]
        )
        def test_login_failure_scenarios(
            self,
            user_service,
            mock_user_repo,
            email,
            password,
            expected_exception
        ):
            """参数化测试各种登录失败场景"""
            
            # ========== Arrange ==========
            if email == "notfound@example.com":
                mock_user_repo.find_by_email.return_value = None
            elif email == "user@example.com":
                mock_user = Mock()
                mock_user.password_hash = "hashed_correct_password"
                mock_user_repo.find_by_email.return_value = mock_user

            # ========== Act & Assert ==========
            with pytest.raises(expected_exception):
                user_service.login(email, password)


# ========== 覆盖率豁免示例 ==========
# @pytest.mark.no_cover  # 标记不计算覆盖率的测试
def test_integration_slow(self):
    """慢速集成测试(可选运行)"""
    pass

5.2 pytest 配置文件

"""
pytest 配置文件
"""
import pytest
from datetime import datetime


# ========== pytest 钩子函数 ==========

def pytest_configure(config):
    """pytest 配置初始化"""
    config.addinivalue_line(
        "markers",
        "slow: marks tests as slow (deselect with '-m \"not slow\"')"
    )
    config.addinivalue_line(
        "markers",
        "integration: marks tests as integration tests"
    )
    config.addinivalue_line(
        "markers",
        "no_cover: marks tests to exclude from coverage"
    )


# ========== Fixtures ==========

@pytest.fixture(scope="session")
def test_config(request):
    """会话级测试配置 fixture"""
    return {
        "database_url": "sqlite:///:memory:",
        "test_timeout": 30,
        "debug_mode": False
    }


@pytest.fixture(scope="function")
def clean_database(test_config):
    """每个测试前清理数据库"""
    # Setup: 初始化数据库
    db = initialize_test_db(test_config["database_url"])
    yield db
    # Teardown: 清理数据库
    cleanup_test_db(db)


# ========== 命令行选项 ==========

def pytest_addoption(parser):
    """添加自定义命令行选项"""
    parser.addoption(
        "--run-integration",
        action="store_true",
        default=False,
        help="run integration tests"
    )


def pytest_collection_modifyitems(config, items):
    """修改测试收集"""
    if not config.getoption("--run-integration"):
        # 跳过集成测试
        skip_integration = pytest.mark.skip(reason="need --run-integration option to run")
        for item in items:
            if "integration" in item.keywords:
                item.add_marker(skip_integration)

🎨 第六章:前端/Jest 测试模板

6.1 React 组件测试模板

/**
 * LoginForm 组件单元测试
 * 
 * @author AI Coding Agent
 * @date 2026-03-13
 * @version 1.0
 */

import React from 'react';
import { render, screen, fireEvent, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import LoginForm from './LoginForm';
import * as authApi from '../../api/auth';

/**
 * 创建测试渲染器包装器
 */
const createWrapper = () => {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: { retry: false },
    },
  });

  return ({ children }) => (
    <QueryClientProvider client={queryClient}>
      <MemoryRouter>{children}</MemoryRouter>
    </QueryClientProvider>
  );
};

describe('LoginForm', () => {
  // ========== Mock 设置 ==========
  beforeEach(() => {
    jest.clearAllMocks();
  });

  // ========== 渲染测试 ==========
  describe('渲染', () => {
    test('应该正确渲染表单元素', () => {
      // Arrange
      const wrapper = createWrapper();

      // Act
      render(<LoginForm />, { wrapper });

      // Assert
      expect(screen.getByPlaceholderText(/邮箱/i)).toBeInTheDocument();
      expect(screen.getByPlaceholderText(/密码/i)).toBeInTheDocument();
      expect(screen.getByRole('button', { name: /登录/i })).toBeInTheDocument();
      expect(screen.getByText(/忘记密码?/i)).toBeInTheDocument();
    });

    test('应该显示必填项标记', () => {
      const wrapper = createWrapper();
      render(<LoginForm />, { wrapper });

      const emailInput = screen.getByPlaceholderText(/邮箱/i);
      const passwordInput = screen.getByPlaceholderText(/密码/i);

      expect(emailInput).toBeRequired();
      expect(passwordInput).toBeRequired();
    });
  });

  // ========== 表单验证测试 ==========
  describe('表单验证', () => {
    test('提交空表单时应该显示验证错误', async () => {
      const user = userEvent.setup();
      const wrapper = createWrapper();
      render(<LoginForm />, { wrapper });

      // Act
      await user.click(screen.getByRole('button', { name: /登录/i }));

      // Assert
      await waitFor(() => {
        expect(screen.getByText(/请输入邮箱/i)).toBeInTheDocument();
        expect(screen.getByText(/请输入密码/i)).toBeInTheDocument();
      });
    });

    test('邮箱格式不正确时应该显示错误', async () => {
      const user = userEvent.setup();
      const wrapper = createWrapper();
      render(<LoginForm />, { wrapper });

      // Act
      await user.type(screen.getByPlaceholderText(/邮箱/i), 'invalid-email');
      await user.type(screen.getByPlaceholderText(/密码/i), 'password123');
      await user.click(screen.getByRole('button', { name: /登录/i }));

      // Assert
      await waitFor(() => {
        expect(screen.getByText(/邮箱格式不正确/i)).toBeInTheDocument();
      });
    });

    test('密码长度不足时应该显示错误', async () => {
      const user = userEvent.setup();
      const wrapper = createWrapper();
      render(<LoginForm />, { wrapper });

      // Act
      await user.type(screen.getByPlaceholderText(/邮箱/i), 'test@example.com');
      await user.type(screen.getByPlaceholderText(/密码/i), '123');
      await user.click(screen.getByRole('button', { name: /登录/i }));

      // Assert
      await waitFor(() => {
        expect(screen.getByText(/密码长度至少为 6 位/i)).toBeInTheDocument();
      });
    });
  });

  // ========== 成功登录测试 ==========
  describe('成功登录', () => {
    test('使用有效凭据应该成功登录并跳转', async () => {
      const user = userEvent.setup();
      const wrapper = createWrapper();
      
      // Mock API 响应
      const mockLogin = jest.spyOn(authApi, 'login').mockResolvedValue({
        token: 'mock_jwt_token',
        user: { id: 1, username: 'test_user' },
      });

      render(<LoginForm />, { wrapper });

      // Act
      await user.type(screen.getByPlaceholderText(/邮箱/i), 'test@example.com');
      await user.type(screen.getByPlaceholderText(/密码/i), 'SecurePass123!');
      await user.click(screen.getByRole('button', { name: /登录/i }));

      // Assert
      await waitFor(() => {
        expect(mockLogin).toHaveBeenCalledWith({
          email: 'test@example.com',
          password: 'SecurePass123!',
        });
        expect(screen.getByText(/登录成功/i)).toBeInTheDocument();
      });

      // 验证路由跳转
      await waitFor(() => {
        expect(window.location.pathname).toBe('/dashboard');
      });
    });
  });

  // ========== 错误处理测试 ==========
  describe('错误处理', () => {
    test('登录失败时应该显示错误消息', async () => {
      const user = userEvent.setup();
      const wrapper = createWrapper();
      
      // Mock API 错误
      const mockLogin = jest.spyOn(authApi, 'login').mockRejectedValue(
        new Error('邮箱或密码错误')
      );

      render(<LoginForm />, { wrapper });

      // Act
      await user.type(screen.getByPlaceholderText(/邮箱/i), 'test@example.com');
      await user.type(screen.getByPlaceholderText(/密码/i), 'wrong_password');
      await user.click(screen.getByRole('button', { name: /登录/i }));

      // Assert
      await waitFor(() => {
        expect(mockLogin).toHaveBeenCalled();
        expect(screen.getByText(/邮箱或密码错误/i)).toBeInTheDocument();
      });
    });

    test('网络错误时应该显示友好提示', async () => {
      const user = userEvent.setup();
      const wrapper = createWrapper();
      
      jest.spyOn(authApi, 'login').mockRejectedValue(
        new TypeError('Network request failed')
      );

      render(<LoginForm />, { wrapper });

      // Act
      await user.type(screen.getByPlaceholderText(/邮箱/i), 'test@example.com');
      await user.type(screen.getByPlaceholderText(/密码/i), 'password123');
      await user.click(screen.getByRole('button', { name: /登录/i }));

      // Assert
      await waitFor(() => {
        expect(screen.getByText(/网络连接失败,请检查网络设置/i)).toBeInTheDocument();
      });
    });
  });

  // ========== 用户体验测试 ==========
  describe('用户体验', () => {
    test('提交表单时按钮应该显示加载状态', async () => {
      const user = userEvent.setup();
      const wrapper = createWrapper();
      
      // Mock 延迟的 API 响应
      const mockLogin = jest.spyOn(authApi, 'login').mockImplementation(
        () => new Promise(resolve => setTimeout(resolve, 1000))
      );

      render(<LoginForm />, { wrapper });

      // Act
      await user.type(screen.getByPlaceholderText(/邮箱/i), 'test@example.com');
      await user.type(screen.getByPlaceholderText(/密码/i), 'password123');
      await user.click(screen.getByRole('button', { name: /登录/i }));

      // Assert
      const submitButton = screen.getByRole('button', { name: /登录/i });
      expect(submitButton).toBeDisabled();
      expect(submitButton).toHaveTextContent(/登录中.../i);

      // 等待请求完成
      await waitFor(() => expect(mockLogin).toHaveBeenCalled());
    });

    test('支持按 Enter 键提交表单', async () => {
      const user = userEvent.setup();
      const wrapper = createWrapper();
      
      const mockLogin = jest.spyOn(authApi, 'login').mockResolvedValue({
        token: 'token',
        user: {},
      });

      render(<LoginForm />, { wrapper });

      // Act
      await user.type(screen.getByPlaceholderText(/邮箱/i), 'test@example.com');
      await user.type(screen.getByPlaceholderText(/密码/i), 'password123');
      await user.keyboard('{Enter}');

      // Assert
      await waitFor(() => {
        expect(mockLogin).toHaveBeenCalled();
      });
    });
  });

  // ========== 无障碍访问测试 ==========
  describe('无障碍访问', () => {
    test('所有表单字段都应该有可访问的标签', () => {
      const wrapper = createWrapper();
      render(<LoginForm />, { wrapper });

      const emailInput = screen.getByPlaceholderText(/邮箱/i);
      const passwordInput = screen.getByPlaceholderText(/密码/i);

      expect(emailInput).toHaveAccessibleName();
      expect(passwordInput).toHaveAccessibleName();
    });

    test('错误消息应该对屏幕阅读器可见', async () => {
      const user = userEvent.setup();
      const wrapper = createWrapper();
      render(<LoginForm />, { wrapper });

      // Act
      await user.click(screen.getByRole('button', { name: /登录/i }));

      // Assert
      await waitFor(() => {
        const errorMessages = screen.getAllByRole('alert');
        expect(errorMessages.length).toBeGreaterThan(0);
      });
    });
  });
});

6.2 Jest 配置文件

/**
 * Jest 配置文件
 */
module.exports = {
  // 测试环境
  testEnvironment: 'jsdom',
  
  // 测试文件匹配模式
  testMatch: [
    '**/__tests__/**/*.test.[jt]s?(x)',
    '**/?(*.)+(spec|test).[jt]s?(x)'
  ],
  
  // 模块文件扩展名
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
  
  // 模块路径映射
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '\\.css$': 'identity-obj-proxy',
    '\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.js'
  },
  
  // 转换配置
  transform: {
    '^.+\\.tsx?$': 'ts-jest',
    '^.+\\.jsx?$': 'babel-jest'
  },
  
  // 忽略转换的目录
  transformIgnorePatterns: [
    '/node_modules/(?!(axios|react-markdown)/)'
  ],
  
  // 覆盖率配置
  collectCoverage: true,
  collectCoverageFrom: [
    'src/**/*.{ts,tsx,js,jsx}',
    '!src/**/*.d.ts',
    '!src/index.tsx',
    '!src/reportWebVitals.ts'
  ],
  coverageThreshold: {
    global: {
      branches: 85,
      functions: 95,
      lines: 90,
      statements: 90
    }
  },
  coverageReporters: [
    'text',
    'lcov',
    'html',
    'clover'
  ],
  
  // 测试报告
  reporters: [
    'default',
    ['jest-html-reporter', {
      pageTitle: '测试报告',
      outputPath: 'reports/test-report.html',
      includeFailureMsg: true
    }]
  ],
  
  // 设置文件
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
  
  // 超时时间
  testTimeout: 10000,
  
  // 并行执行
  maxWorkers: '50%',
  
  // 详细输出
  verbose: true,
  
  // 清除 Mock
  clearMocks: true,
  resetMocks: true,
  restoreMocks: true
};

📋 第七章:测试用例设计清单

7.1 测试场景完整性检查清单

测试用例设计检查清单(AI Coding Agent 自动生成测试时必须覆盖)
序号 检查项 优先级 说明与示例 AI 自动生成
1 正常流程测试
Happy Path
P0 验证业务主流程正常工作
例:用户注册成功、订单创建成功
✅ 必须
2 异常流程测试
Exception Handling
P0 验证各种异常情况的处理
例:资源不存在、权限不足、数据冲突
✅ 必须
3 边界值测试
Boundary Values
P1 验证边界条件的处理
例:最小值、最大值、空值、零值
✅ 必须
4 等价类划分
Equivalence Classes
P1 从每个等价类中选择代表性测试数据
例:有效邮箱、无效邮箱格式
✅ 必须
5 状态转换测试
State Transition
P2 验证对象状态变化的正确性
例:订单状态从 PENDING→PAID→SHIPPED
✅ 推荐
6 依赖 Mock 测试
Mock Dependencies
P1 验证与外部依赖的交互
例:数据库调用、API 调用、消息队列
✅ 必须
7 副作用验证
Side Effects
P2 验证操作的间接影响
例:缓存更新、事件发布、日志记录
✅ 推荐
8 并发场景测试
Concurrency
P2 验证并发操作的正确性
例:并发下单、库存扣减
⚠️ 选择性
9 性能相关测试
Performance
P3 验证基本性能要求
例:大数据量处理、循环效率
⚠️ 选择性
10 安全性测试
Security
P1 验证安全机制
例:SQL 注入防护、XSS 防护、权限校验
✅ 必须
11 国际化测试
i18n
P3 验证多语言支持
例:特殊字符、RTL 语言、日期格式
⚠️ 按需
12 回归测试
Regression
P2 确保修复不引入新问题
例:Bug 修复后的验证
✅ 推荐

7.2 测试数据设计原则

📊 测试数据设计规范:
  • 真实性:使用贴近生产环境的真实数据格式
  • 独立性:每个测试用例使用独立的数据,避免相互影响
  • 可重复性:测试数据应该是确定性的,保证测试结果可重现
  • 可读性:测试数据应该清晰表达测试意图
  • 边界覆盖:必须包含边界值、极值、特殊值
  • 数据清理:测试后必须清理数据,保持环境干净

🔍 第八章:覆盖率报告解读

8.1 JaCoCo 覆盖率报告示例

📊 JaCoCo 覆盖率报告解读指南
================================================================================
JaCoCo 代码覆盖率报告
================================================================================
项目:User Management Service
构建:#2026.03.13-001
时间:2026-03-13 10:30:00
================================================================================

【总体覆盖率统计】
┌──────────────────┬──────────────┬──────────────┬──────────────┬──────────────┐
│ 指标             │ 覆盖数       │ 总数         │ 覆盖率       │ 要求         │
├──────────────────┼──────────────┼──────────────┼──────────────┼──────────────┤
│ 行覆盖率 (Lines) │ 1,245        │ 1,280        │ 97.26%       │ ≥95% ✅      │
│ 分支覆盖率       │ 456          │ 485          │ 94.02%       │ ≥90% ✅      │
│ 函数覆盖率       │ 89           │ 92           │ 96.74%       │ ≥95% ✅      │
│ 类覆盖率         │ 28           │ 30           │ 93.33%       │ ≥90% ✅      │
└──────────────────┴──────────────┴──────────────┴──────────────┴──────────────┘

【按模块统计】
┌────────────────────────────┬──────────────┬──────────────┬──────────────┐
│ 模块名称                   │ 行覆盖率     │ 分支覆盖率   │ 状态         │
├────────────────────────────┼──────────────┼──────────────┼──────────────┤
│ service                    │ 98.5%        │ 96.2%        │ ✅ 优秀      │
│ controller                 │ 97.8%        │ 94.5%        │ ✅ 优秀      │
│ repository                 │ 96.2%        │ 92.1%        │ ✅ 良好      │
│ entity                     │ 85.3%        │ N/A          │ ⚠️ 待改进    │
│ config                     │ 78.9%        │ 65.4%        │ ❌ 不达标    │
│ util                       │ 92.1%        │ 88.7%        │ ✅ 良好      │
└────────────────────────────┴──────────────┴──────────────┴──────────────┘

【未覆盖代码详情】
┌─────────────────────────────────────────────────────────────────────────────┐
│ 文件:com.example.config.SecurityConfig.java                                │
│ 覆盖率:78.9% (行), 65.4% (分支)                                            │
│                                                                             │
│ 未覆盖行:                                                                   │
│ - 第 45-52 行:configure(HttpSecurity http) 方法中的 CORS 配置分支             │
│ - 第 78-85 行:passwordEncoder() Bean 的备选方案                              │
│                                                                             │
│ 原因分析:                                                                   │
│ 1. CORS 配置仅在特定环境下启用,测试环境未触发                                │
│ 2. passwordEncoder 有多个实现,测试只覆盖了 BCrypt                           │
│                                                                             │
│ 改进建议:                                                                   │
│ 1. 添加不同环境配置下的集成测试                                              │
│ 2. 增加多种密码编码器的测试用例                                              │
└─────────────────────────────────────────────────────────────────────────────┘

【覆盖率趋势】
┌──────────┬──────────────┬──────────────┬──────────────┐
│ 构建版本 │ 行覆盖率     │ 分支覆盖率   │ 变化趋势     │
├──────────┼──────────────┼──────────────┼──────────────┤
│ #001     │ 92.3%        │ 88.5%        │ -            │
│ #002     │ 94.1%        │ 90.2%        │ ↑ +1.8%      │
│ #003     │ 95.8%        │ 92.7%        │ ↑ +1.7%      │
│ #004     │ 96.5%        │ 93.4%        │ ↑ +0.7%      │
│ #005     │ 97.26%       │ 94.02%       │ ↑ +0.76%     │
└──────────┴──────────────┴──────────────┴──────────────┘

【质量门禁判定】
┌─────────────────────────────────────────────────────────────────────────────┐
│ ✅ 行覆盖率 97.26% ≥ 95% - 通过                                             │
│ ✅ 分支覆盖率 94.02% ≥ 90% - 通过                                           │
│ ✅ 函数覆盖率 96.74% ≥ 95% - 通过                                           │
│ ✅ 新增代码覆盖率 98.1% ≥ 95% - 通过                                        │
│                                                                             │
│ 🎉 综合判定:BUILD PASSED - 允许合并                                        │
└─────────────────────────────────────────────────────────────────────────────┘

================================================================================

8.2 覆盖率问题分析与改进

⚠️ 常见覆盖率问题及解决方案:
  • 问题 1:Getter/Setter 未覆盖
    • 原因:Lombok 自动生成,无需手动测试
    • 解决:使用@Generated注解排除,或配置 JaCoCo 忽略
  • 问题 2:异常处理分支未覆盖
    • 原因:异常场景难以触发
    • 解决:使用 Mock 抛出异常,或使用@Test(expected=...)
  • 问题 3:私有方法无法直接测试
    • 原因:私有方法只能通过公开方法间接测试
    • 解决:通过公开方法覆盖所有路径,或考虑重构为包级可见
  • 问题 4:静态方法难以 Mock
    • 原因:传统 Mock 框架不支持静态方法
    • 解决:使用 PowerMock 或 Mockito Inline,或重构为实例方法

✅ 第九章:AI 自动生成测试规范

9.1 Claude Code 生成测试指令模板

🤖 AI Coding Agent 生成测试的标准 Prompt 模板
"""
请为以下代码生成完整的单元测试套件:

【要求】
1. 遵循 AAA (Arrange-Act-Assert) 模式
2. 使用 [JUnit 5/pytest/Jest] 测试框架
3. 覆盖率要求:
   - 行覆盖率 ≥ 95%
   - 分支覆盖率 ≥ 90%
   - 函数覆盖率 ≥ 95%

【测试场景覆盖】
✅ 必须覆盖:
- 正常流程测试(Happy Path)
- 异常流程测试(所有可能的异常)
- 边界值测试(最小值、最大值、空值、零值)
- 等价类划分(有效/无效输入)
- Mock 依赖交互验证

⚠️ 选择性覆盖:
- 并发场景(如适用)
- 性能测试(如适用)
- 安全性测试(如适用)

【代码风格】
- 测试方法命名:test[MethodName]_[Scenario]_[ExpectedResult]
- 每个测试方法不超过 50 行
- 使用 descriptive variable names
- 添加必要的注释说明测试意图

【输出格式】
1. 完整的测试类代码
2. 必要的 Mock 配置
3. Test Fixture 设置
4. 覆盖率预估说明

【被测代码】
[在此粘贴需要测试的源代码]
"""

9.2 AI 生成测试质量检查清单

🔍 AI 生成测试质量检查(Tech Lead 审查要点):
  • 测试是否遵循 AAA 模式
  • 是否覆盖了所有公共方法
  • 异常场景是否完整
  • 边界值是否测试
  • Mock 配置是否合理
  • 断言是否充分且准确
  • 测试之间是否独立
  • 测试数据是否具有代表性
  • 是否有冗余测试
  • 测试执行时间是否合理

📈 第十章:质量门禁与持续改进

10.1 CI/CD 质量门禁配置

# Jenkins Pipeline 质量门禁配置
pipeline {
    agent any
    
    stages {
        stage('Unit Test & Coverage') {
            steps {
                script {
                    // 运行单元测试并生成覆盖率报告
                    sh 'mvn clean test jacoco:report'
                    
                    // 检查覆盖率是否达标
                    def coverageReport = jacocoParse()
                    
                    // 质量门禁检查
                    if (coverageReport.lineCoverage < 95) {
                        error("❌ 行覆盖率 ${coverageReport.lineCoverage}% < 95%,构建失败")
                    }
                    if (coverageReport.branchCoverage < 90) {
                        error("❌ 分支覆盖率 ${coverageReport.branchCoverage}% < 90%,构建失败")
                    }
                    if (coverageReport.functionCoverage < 95) {
                        error("❌ 函数覆盖率 ${coverageReport.functionCoverage}% < 95%,构建失败")
                    }
                    
                    echo "✅ 覆盖率检查通过!"
                }
            }
            post {
                always {
                    jacoco execPattern: '**/target/jacoco.exec'
                }
            }
        }
        
        stage('Quality Gate') {
            steps {
                script {
                    // SonarQube 质量门禁
                    timeout(time: 10, unit: 'MINUTES') {
                        waitForQualityGate abortPipeline: true
                    }
                }
            }
        }
    }
}

10.2 持续改进机制

📊
每周覆盖率报告
每周一自动生成全项目覆盖率报告,识别覆盖率下降的模块,责任到人限期整改。
🎯
月度质量评审
每月召开质量评审会议,分析测试覆盖率趋势,分享最佳实践,制定改进计划。
🏆
质量激励机制
设立"质量之星"奖项,表彰覆盖率持续达标的团队和个人,纳入绩效考核。
🔄
技术债务管理
建立技术债务台账,优先偿还高风险模块的测试债务,每个迭代预留 20% 时间用于质量改进。
🎉 实施效果预期:
  • 3 个月内:全项目平均覆盖率从当前水平提升至 90%+
  • 6 个月内:核心模块覆盖率达到 95%+,生产 Bug 率下降 50%
  • 1 年内:建立完善的测试文化,测试成为开发习惯而非负担