基于 OpenClaw + Claude Code 的端到端研发自动化系统 · 质量保障体系核心规范
代码覆盖率是衡量测试完整性的关键指标,本系统采用以下四种覆盖率度量标准:
| 模块类型 | 行覆盖率 | 分支覆盖率 | 函数覆盖率 | 优先级 | 验收标准 |
|---|---|---|---|---|---|
| 核心业务逻辑 支付、交易、认证等 |
≥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 - 可选 | 豁免申请 |
以下情况可申请覆盖率豁免,但必须经过 Tech Lead 和安全团队双重审批:
// 示例:豁免申请的代码注释标记 // @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 (Arrange-Act-Assert) 模式编写所有单元测试,确保测试代码的可读性、一致性和可维护性。
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)); } }
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()
================================================================================ 测试用例标识符: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 配置合理。 │ └──────────┴────────────┴───────────────────────────────────────────────────────┘ ================================================================================
test[MethodName]_[Scenario]_[ExpectedResult]testRegisterUser_Success_ReturnsUserObjecttestRegisterUser_UsernameExists_ThrowsIllegalArgumentExceptiontest 用户注册 _ 成功 _ 返回用户对象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 ArgumentCaptororderCaptor = ArgumentCaptor.forClass(Order.class); verify(orderRepository).save(orderCaptor.capture()); Order savedOrder = orderCaptor.getValue(); assertEquals(OrderStatus.PAID, savedOrder.getStatus()); assertEquals(transactionId, savedOrder.getTransactionId()); } } }
<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>
""" 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
""" 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)
/** * 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); }); }); }); });
/** * 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 };
| 序号 | 检查项 | 优先级 | 说明与示例 | 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 修复后的验证 |
✅ 推荐 |
================================================================================ 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 - 允许合并 │ └─────────────────────────────────────────────────────────────────────────────┘ ================================================================================
@Generated注解排除,或配置 JaCoCo 忽略@Test(expected=...)"""
请为以下代码生成完整的单元测试套件:
【要求】
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. 覆盖率预估说明
【被测代码】
[在此粘贴需要测试的源代码]
"""
# 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 } } } } } }