集成测试自动化:用 Claude Skills 构建可靠的系统测试体系

集成测试自动化:用 Claude Skills 构建可靠的系统测试体系,确保模块协作无障碍

前言

解决如下问题:

  • 单元测试全部通过,但集成到一起就各种报错?
  • API 接口返回数据格式变更,导致多个模块崩溃?
  • 数据库事务回滚失败,数据一致性无法保证?
  • 第三方服务不稳定,整个系统受影响?
  • 微服务架构下,服务间调用链复杂难以测试?

作为一名后端开发者,我们都知道集成测试的重要性,但在实际项目中,集成测试往往被忽视或草草了事。今天,我要介绍一个彻底改变集成测试现状的解决方案------集成测试自动化 Skill

通过将集成测试的最佳实践封装成 Skill,我们可以让 Claude 严格按照团队标准自动生成高质量的集成测试代码,包括数据库集成测试、API 集成测试、服务间集成测试等。不仅大幅提升测试效率,还能保证系统的可靠性和稳定性。

一、为什么需要规范的集成测试体系?

1.1 集成测试的核心价值

集成测试是连接单元测试和 E2E 测试的桥梁,具有以下核心价值:

  • 验证模块协作:确保各个模块协同工作正常
  • 检测集成问题:发现单元测试无法暴露的集成错误
  • 验证接口契约:确保 API 接口符合设计规范
  • 测试数据流:验证数据在不同模块间的流转正确性
  • 检测配置问题:发现配置错误导致的问题
  • 验证外部依赖:测试与数据库、缓存、消息队列等的集成
  • 保障系统稳定性:提前发现系统性问题

1.2 传统集成测试的痛点

问题 影响
测试环境不稳定 偶发性失败,降低测试可信度
测试执行慢 影响开发效率,延迟反馈
外部依赖复杂 数据库、第三方服务难以 Mock
测试数据准备难 需要复杂的测试数据构建
测试清理不彻底 残留数据影响后续测试
覆盖率不完整 只测试 happy path,异常场景遗漏

核心理念:好的集成测试不是"越多越好",而是"精准高效"。通过 Claude Skills,我们可以建立科学的集成测试策略,在测试投入和系统质量之间找到最佳平衡点。

二、创建集成测试自动化 Skill

2.1 准备工作

首先,创建 Skill 目录:

bash 复制代码
mkdir -p .claude/skills/integration-test

2.2 编写 SKILL.md

integration-test 文件夹下创建 SKILL.md 文件,采用 YAML Frontmatter + Markdown 正文 的格式。

YAML 元数据
yaml 复制代码
---
name: integration-test
description: 生成集成测试代码,包括数据库集成测试、API集成测试、服务间集成测试等。支持 Spring Boot、Express、FastAPI 等主流框架。当用户需要编写集成测试、测试模块间协作、验证接口契约时使用。
allowed-tools: Read, Bash
version: 1.0.0
---
主体内容结构
markdown 复制代码
## 触发条件
(说明什么情况下激活此技能)

## 集成测试类型
(不同类型集成测试的适用场景)

## 集成测试编写规范
(数据库测试、API测试、服务间测试的编写原则)

## 测试环境配置
(测试数据库、Mock服务器、测试数据准备)

## 测试数据管理
(测试数据的创建、清理、隔离策略)

## Mock策略
(外部依赖Mock、服务Mock、第三方库Mock)

## 事务管理
(数据库事务回滚、测试隔离)

## 常见测试场景
(CRUD操作、事务一致性、异常处理等场景)

## 常见问题与解决方案
(集成测试编写和调试过程中的常见问题)

2.3 核心:集成测试类型与触发条件

集成测试类型
markdown 复制代码
## 集成测试类型

根据测试范围和复杂度,集成测试分为以下类型:

### 1. 数据库集成测试
- **测试对象**:数据访问层(DAO/Repository)
- **测试范围**:SQL查询、事务管理、数据一致性
- **常用工具**:TestContainers、H2、JUnit、pytest
- **执行速度**:中等(秒级到分钟级)
- **比例建议**:30% 左右

**特点**:
- 使用真实的测试数据库
- 测试 CRUD 操作和复杂查询
- 验证事务回滚和并发控制
- 检查索引和约束

### 2. API 集成测试
- **测试对象**:REST API、GraphQL API
- **测试范围**:接口响应、数据格式、错误处理
- **常用工具**:RestAssured、Supertest、pytest-httpx
- **执行速度**:快(秒级)
- **比例建议**:35% 左右

**特点**:
- 测试完整的请求-响应周期
- 验证 HTTP 状态码和响应体
- 测试认证和授权
- 验证 API 版本兼容性

### 3. 服务间集成测试
- **测试对象**:微服务、模块间调用
- **测试范围**:服务调用链、消息传递、事件处理
- **常用工具**:TestContainers、WireMock、Kafka Testcontainers
- **执行速度**:慢(分钟级)
- **比例建议**:15% 左右

**特点**:
- 测试多个服务的协作
- 验证消息队列集成
- 测试服务发现和负载均衡
- 模拟网络故障和超时

### 4. 缓存集成测试
- **测试对象**:Redis、Memcached 集成
- **测试范围**:缓存读写、过期策略、缓存穿透
- **常用工具**:Redis Testcontainers、Embedded Redis
- **执行速度**:快(秒级)
- **比例建议**:10% 左右

**特点**:
- 测试缓存读写性能
- 验证缓存一致性
- 测试缓存失效策略
- 检查缓存雪崩和击穿

### 5. 消息队列集成测试
- **测试对象**:Kafka、RabbitMQ 集成
- **测试范围**:消息发送、消费、重试机制
- **常用工具**:Kafka Testcontainers、Embedded RabbitMQ
- **执行速度**:中等(秒级)
- **比例建议**:10% 左右

**特点**:
- 测试消息发送和消费
- 验证消息顺序和幂等性
- 测试死信队列
- 验证消息持久化
触发条件定义
markdown 复制代码
## 触发条件

当用户提出以下需求时激活此技能:

- "帮我写集成测试"
- "数据库集成测试"
- "API 集成测试"
- "服务间测试"
- "集成测试用例"
- "测试模块间协作"
- "Mock 服务测试"
- "事务测试"
- "集成测试覆盖率"

三、集成测试编写规范详解

3.1 数据库集成测试

3.1.1 Spring Boot 数据库集成测试
markdown 复制代码
## Spring Boot 数据库集成测试

使用 Spring Boot Test 和 TestContainers 进行真实的数据库测试。

### 配置示例

​```java
// application-test.properties
spring.datasource.url=jdbc:tc:postgresql:15:///testdb
spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
spring.kafka.bootstrap-servers=${spring.embedded.kafka.brokers}

基础测试模板

java 复制代码
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.transaction.annotation.Transactional;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class UserRepositoryIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
            .withDatabaseName("testdb")
            .withUsername("testuser")
            .withPassword("testpass");

    @DynamicPropertySource
    static void postgresProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private TestEntityManager entityManager;

    private User testUser;

    @BeforeEach
    void setUp() {
        testUser = User.builder()
                .username("testuser")
                .email("test@example.com")
                .password("password123")
                .build();
    }

    @Test
    @Order(1)
    @DisplayName("应该成功保存用户")
    void shouldSaveUserSuccessfully() {
        // When
        User savedUser = userRepository.save(testUser);

        // Then
        assertThat(savedUser).isNotNull();
        assertThat(savedUser.getId()).isNotNull();
        assertThat(savedUser.getUsername()).isEqualTo("testuser");
        assertThat(savedUser.getEmail()).isEqualTo("test@example.com");
        assertThat(savedUser.getCreatedAt()).isNotNull();
    }

    @Test
    @Order(2)
    @DisplayName("应该通过ID查找用户")
    void shouldFindUserById() {
        // Given
        User savedUser = userRepository.save(testUser);

        // When
        Optional<User> foundUser = userRepository.findById(savedUser.getId());

        // Then
        assertThat(foundUser).isPresent();
        assertThat(foundUser.get().getUsername()).isEqualTo("testuser");
    }

    @Test
    @Order(3)
    @DisplayName("应该通过用户名查找用户")
    void shouldFindByUsername() {
        // Given
        userRepository.save(testUser);

        // When
        Optional<User> foundUser = userRepository.findByUsername("testuser");

        // Then
        assertThat(foundUser).isPresent();
        assertThat(foundUser.get().getEmail()).isEqualTo("test@example.com");
    }

    @Test
    @Order(4)
    @DisplayName("应该更新用户信息")
    void shouldUpdateUser() {
        // Given
        User savedUser = userRepository.save(testUser);
        savedUser.setEmail("updated@example.com");

        // When
        User updatedUser = userRepository.save(savedUser);

        // Then
        assertThat(updatedUser.getEmail()).isEqualTo("updated@example.com");
    }

    @Test
    @Order(5)
    @DisplayName("应该删除用户")
    void shouldDeleteUser() {
        // Given
        User savedUser = userRepository.save(testUser);

        // When
        userRepository.deleteById(savedUser.getId());

        // Then
        Optional<User> deletedUser = userRepository.findById(savedUser.getId());
        assertThat(deletedUser).isEmpty();
    }

    @Test
    @DisplayName("应该验证邮箱唯一性约束")
    void shouldValidateEmailUniqueness() {
        // Given
        User user1 = User.builder()
                .username("user1")
                .email("same@example.com")
                .password("password123")
                .build();
        User user2 = User.builder()
                .username("user2")
                .email("same@example.com")
                .password("password456")
                .build();

        // When & Then
        userRepository.save(user1);
        
        Assertions.assertThrows(Exception.class, () -> {
            userRepository.save(user2);
        });
    }

    @Test
    @Transactional
    @DisplayName("应该测试事务回滚")
    void shouldTestTransactionRollback() {
        // Given
        User savedUser = userRepository.save(testUser);

        // When
        Assertions.assertThrows(RuntimeException.class, () -> {
            userRepository.save(savedUser); // 尝试保存重复用户
        });

        // Then - 事务应该回滚,用户不应该被修改
        entityManager.flush();
        entityManager.clear();
        
        Optional<User> userAfter = userRepository.findById(savedUser.getId());
        assertThat(userAfter).isPresent();
        assertThat(userAfter.get().getVersion()).isEqualTo(savedUser.getVersion());
    }

    @Test
    @DisplayName("应该测试复杂查询")
    void shouldTestComplexQuery() {
        // Given - 准备测试数据
        User user1 = User.builder()
                .username("user1")
                .email("user1@example.com")
                .password("password1")
                .status(UserStatus.ACTIVE)
                .build();
        User user2 = User.builder()
                .username("user2")
                .email("user2@example.com")
                .password("password2")
                .status(UserStatus.INACTIVE)
                .build();
        User user3 = User.builder()
                .username("user3")
                .email("user3@example.com")
                .password("password3")
                .status(UserStatus.ACTIVE)
                .build();

        userRepository.saveAll(List.of(user1, user2, user3));

        // When
        List<User> activeUsers = userRepository.findByStatus(UserStatus.ACTIVE);

        // Then
        assertThat(activeUsers).hasSize(2);
        assertThat(activeUsers)
                .extracting(User::getUsername)
                .containsExactlyInAnyOrder("user1", "user3");
    }

    @Test
    @DisplayName("应该测试分页查询")
    void shouldTestPagination() {
        // Given - 创建 20 个用户
        List<User> users = new ArrayList<>();
        for (int i = 1; i <= 20; i++) {
            users.add(User.builder()
                    .username("user" + i)
                    .email("user" + i + "@example.com")
                    .password("password" + i)
                    .build());
        }
        userRepository.saveAll(users);

        // When - 查询第一页,每页10条
        Pageable pageable = PageRequest.of(0, 10);
        Page<User> userPage = userRepository.findAll(pageable);

        // Then
        assertThat(userPage.getTotalElements()).isEqualTo(20);
        assertThat(userPage.getTotalPages()).isEqualTo(2);
        assertThat(userPage.getContent()).hasSize(10);
        assertThat(userPage.isFirst()).isTrue();
        assertThat(userPage.hasNext()).isTrue();
    }
}

复杂关联关系测试

java 复制代码
@SpringBootTest
@Testcontainers
class OrderRepositoryIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine");

    @DynamicPropertySource
    static void postgresProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private UserRepository userRepository;

    @Test
    @Transactional
    @DisplayName("应该测试一对多关联关系 - 订单与订单项")
    void shouldTestOneToManyRelationship() {
        // Given
        User user = userRepository.save(User.builder()
                .username("testuser")
                .email("test@example.com")
                .password("password123")
                .build());

        Product product1 = productRepository.save(Product.builder()
                .name("商品1")
                .price(99.99)
                .stock(100)
                .build());

        Product product2 = productRepository.save(Product.builder()
                .name("商品2")
                .price(199.99)
                .stock(50)
                .build());

        List<OrderItem> items = List.of(
                OrderItem.builder()
                        .product(product1)
                        .quantity(2)
                        .price(product1.getPrice())
                        .build(),
                OrderItem.builder()
                        .product(product2)
                        .quantity(1)
                        .price(product2.getPrice())
                        .build()
        );

        Order order = Order.builder()
                .user(user)
                .orderNumber("ORD123456")
                .items(items)
                .status(OrderStatus.PAID)
                .totalAmount(2 * product1.getPrice() + product2.getPrice())
                .build();

        // When
        Order savedOrder = orderRepository.save(order);

        // Then
        assertThat(savedOrder.getId()).isNotNull();
        assertThat(savedOrder.getItems()).hasSize(2);
        assertThat(savedOrder.getItems())
                .extracting(OrderItem::getProduct)
                .extracting(Product::getName)
                .containsExactlyInAnyOrder("商品1", "商品2");
    }

    @Test
    @DisplayName("应该测试级联删除")
    void shouldTestCascadeDelete() {
        // Given
        Product product = productRepository.save(Product.builder()
                .name("商品")
                .price(99.99)
                .stock(100)
                .build());

        Order order = orderRepository.save(Order.builder()
                .orderNumber("ORD123")
                .status(OrderStatus.PENDING)
                .items(List.of(OrderItem.builder()
                        .product(product)
                        .quantity(1)
                        .price(product.getPrice())
                        .build()))
                .build());

        // When
        orderRepository.deleteById(order.getId());

        // Then
        // 订单项应该被级联删除
        assertThat(orderRepository.findById(order.getId())).isEmpty();
    }

    @Test
    @DisplayName("应该测试 N+1 查询问题")
    void shouldTestNPlusOneQueryProblem() {
        // Given
        User user = userRepository.save(User.builder()
                .username("testuser")
                .email("test@example.com")
                .password("password123")
                .build());

        List<Order> orders = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Order order = Order.builder()
                    .user(user)
                    .orderNumber("ORD" + i)
                    .status(OrderStatus.COMPLETED)
                    .build();
            orders.add(order);
        }
        orderRepository.saveAll(orders);

        // When - 使用 JOIN FETCH 避免 N+1 问题
        List<Order> userOrders = orderRepository.findByUserWithItems(user);

        // Then
        assertThat(userOrders).hasSize(10);
    }
}

测试数据构建器模式

java 复制代码
// UserTestDataBuilder.java
public class UserTestDataBuilder {
    private Long id;
    private String username = "testuser";
    private String email = "test@example.com";
    private String password = "password123";
    private UserStatus status = UserStatus.ACTIVE;

    public static UserTestDataBuilder aUser() {
        return new UserTestDataBuilder();
    }

    public UserTestDataBuilder withId(Long id) {
        this.id = id;
        return this;
    }

    public UserTestDataBuilder withUsername(String username) {
        this.username = username;
        return this;
    }

    public UserTestDataBuilder withEmail(String email) {
        this.email = email;
        return this;
    }

    public UserTestDataBuilder withStatus(UserStatus status) {
        this.status = status;
        return this;
    }

    public User build() {
        return User.builder()
                .id(id)
                .username(username)
                .email(email)
                .password(password)
                .status(status)
                .build();
    }
}

// 使用示例
@Test
void shouldUseTestDataBuilder() {
    // Given
    User user = UserTestDataBuilder.aUser()
            .withUsername("customuser")
            .withEmail("custom@example.com")
            .withStatus(UserStatus.INACTIVE)
            .build();

    // When
    userRepository.save(user);

    // Then
    assertThat(userRepository.findByUsername("customuser")).isPresent();
}
复制代码
#### 3.1.2 Python 数据库集成测试

​```markdown
## Python 数据库集成测试

使用 pytest 和 TestContainers 进行数据库测试。

### 配置示例

​```python
# conftest.py
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from testcontainers.postgres import PostgresContainer

@pytest.fixture(scope="session")
def postgres_container():
    """启动 PostgreSQL 测试容器"""
    container = PostgresContainer("postgres:15-alpine")
    container.start()
    yield container
    container.stop()

@pytest.fixture(scope="function")
def db_session(postgres_container):
    """创建数据库会话"""
    engine = create_engine(postgres_container.get_connection_url())
    Session = sessionmaker(bind=engine)
    
    # 创建表结构
    Base.metadata.create_all(engine)
    
    session = Session()
    yield session
    
    session.close()
    Base.metadata.drop_all(engine)

测试示例

python 复制代码
# test_user_repository.py
import pytest
from models import User, UserStatus
from repositories import UserRepository

@pytest.fixture
def user_repository(db_session):
    """创建用户仓储实例"""
    return UserRepository(db_session)

class TestUserRepository:
    
    @pytest.fixture
    def test_user(self):
        """创建测试用户数据"""
        return User(
            username="testuser",
            email="test@example.com",
            password="hashed_password",
            status=UserStatus.ACTIVE
        )
    
    def test_create_user(self, user_repository, test_user):
        """测试创建用户"""
        # When
        saved_user = user_repository.create(test_user)
        
        # Then
        assert saved_user.id is not None
        assert saved_user.username == "testuser"
        assert saved_user.email == "test@example.com"
        assert saved_user.created_at is not None
    
    def test_find_by_id(self, user_repository, test_user):
        """测试通过 ID 查找用户"""
        # Given
        saved_user = user_repository.create(test_user)
        
        # When
        found_user = user_repository.find_by_id(saved_user.id)
        
        # Then
        assert found_user is not None
        assert found_user.username == test_user.username
    
    def test_find_by_username(self, user_repository, test_user):
        """测试通过用户名查找用户"""
        # Given
        user_repository.create(test_user)
        
        # When
        found_user = user_repository.find_by_username("testuser")
        
        # Then
        assert found_user is not None
        assert found_user.email == test_user.email
    
    def test_update_user(self, user_repository, test_user):
        """测试更新用户"""
        # Given
        saved_user = user_repository.create(test_user)
        saved_user.email = "updated@example.com"
        
        # When
        updated_user = user_repository.update(saved_user)
        
        # Then
        assert updated_user.email == "updated@example.com"
    
    def test_delete_user(self, user_repository, test_user):
        """测试删除用户"""
        # Given
        saved_user = user_repository.create(test_user)
        
        # When
        user_repository.delete(saved_user.id)
        
        # Then
        deleted_user = user_repository.find_by_id(saved_user.id)
        assert deleted_user is None
    
    def test_email_uniqueness_constraint(self, user_repository):
        """测试邮箱唯一性约束"""
        # Given
        user1 = User(
            username="user1",
            email="same@example.com",
            password="password1",
            status=UserStatus.ACTIVE
        )
        user2 = User(
            username="user2",
            email="same@example.com",
            password="password2",
            status=UserStatus.ACTIVE
        )
        
        # When & Then
        user_repository.create(user1)
        with pytest.raises(IntegrityError):
            user_repository.create(user2)
    
    def test_find_by_status(self, user_repository):
        """测试按状态查找用户"""
        # Given
        active_user = User(
            username="active_user",
            email="active@example.com",
            password="password",
            status=UserStatus.ACTIVE
        )
        inactive_user = User(
            username="inactive_user",
            email="inactive@example.com",
            password="password",
            status=UserStatus.INACTIVE
        )
        user_repository.create(active_user)
        user_repository.create(inactive_user)
        
        # When
        active_users = user_repository.find_by_status(UserStatus.ACTIVE)
        
        # Then
        assert len(active_users) == 1
        assert active_users[0].username == "active_user"
    
    def test_transaction_rollback(self, user_repository):
        """测试事务回滚"""
        # Given
        user = user_repository.create(User(
            username="testuser",
            email="test@example.com",
            password="password",
            status=UserStatus.ACTIVE
        ))
        original_email = user.email
        
        # When - 模拟事务失败
        try:
            user.email = "updated@example.com"
            user_repository.update(user)
            raise Exception("模拟异常")
        except Exception:
            pass
        
        # Then - 事务应该回滚
        recovered_user = user_repository.find_by_id(user.id)
        assert recovered_user.email == original_email

复杂查询测试

python 复制代码
class TestOrderRepository:
    
    @pytest.fixture
    def order_repository(self, db_session):
        """创建订单仓储实例"""
        return OrderRepository(db_session)
    
    @pytest.fixture
    def setup_test_data(self, db_session):
        """设置测试数据"""
        # 创建用户
        user = User(
            username="testuser",
            email="test@example.com",
            password="password",
            status=UserStatus.ACTIVE
        )
        db_session.add(user)
        
        # 创建商品
        products = []
        for i in range(1, 4):
            product = Product(
                name=f"商品{i}",
                price=99.99 * i,
                stock=100
            )
            products.append(product)
            db_session.add(product)
        
        # 创建订单
        orders = []
        for i in range(1, 4):
            order = Order(
                user=user,
                order_number=f"ORD{i:03d}",
                status=OrderStatus.COMPLETED if i < 3 else OrderStatus.PENDING,
                total_amount=99.99 * i,
                items=[
                    OrderItem(
                        product=products[i-1],
                        quantity=i,
                        price=99.99 * i
                    )
                ]
            )
            orders.append(order)
            db_session.add(order)
        
        db_session.commit()
        return user, orders, products
    
    def test_find_orders_by_user(self, order_repository, setup_test_data):
        """测试查找用户的订单"""
        user, orders, _ = setup_test_data
        
        # When
        user_orders = order_repository.find_by_user_id(user.id)
        
        # Then
        assert len(user_orders) == 3
        assert all(order.user_id == user.id for order in user_orders)
    
    def test_find_orders_by_status(self, order_repository, setup_test_data):
        """测试按状态查找订单"""
        _, orders, _ = setup_test_data
        
        # When
        completed_orders = order_repository.find_by_status(OrderStatus.COMPLETED)
        
        # Then
        assert len(completed_orders) == 2
    
    def test_find_orders_with_items(self, order_repository, setup_test_data):
        """测试查找订单及其明细"""
        user, orders, _ = setup_test_data
        
        # When
        orders_with_items = order_repository.find_by_user_with_items(user.id)
        
        # Then
        assert len(orders_with_items) == 3
        # 验证订单项已加载
        assert all(len(order.items) > 0 for order in orders_with_items)
    
    def test_calculate_total_amount(self, order_repository, setup_test_data):
        """测试计算订单总金额"""
        _, orders, _ = setup_test_data
        expected_total = sum(order.total_amount for order in orders)
        
        # When
        total = order_repository.calculate_total_amount_by_user(orders[0].user_id)
        
        # Then
        assert total == expected_total
复制代码
### 3.2 API 集成测试

#### 3.2.1 Spring Boot REST API 集成测试

​```markdown
## Spring Boot REST API 集成测试

使用 MockMvc 进行 REST API 集成测试。

### 基础测试模板

​```java
import org.junit.jupiter.api.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import com.fasterxml.jackson.databind.ObjectMapper;

import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@SpringBootTest
@Testcontainers
@AutoConfigureMockMvc
@WithMockUser
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class UserControllerIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
            .withDatabaseName("testdb")
            .withUsername("testuser")
            .withPassword("testpass");

    @DynamicPropertySource
    static void postgresProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private UserRepository userRepository;

    @BeforeEach
    void setUp() {
        userRepository.deleteAll();
    }

    @Test
    @Order(1)
    @DisplayName("POST /api/users - 应该成功创建用户")
    void shouldCreateUserSuccessfully() throws Exception {
        // Given
        UserCreateRequest request = UserCreateRequest.builder()
                .username("testuser")
                .email("test@example.com")
                .password("Password123!")
                .build();

        // When & Then
        mockMvc.perform(post("/api/users")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.code").value(0))
                .andExpect(jsonPath("$.data.id").exists())
                .andExpect(jsonPath("$.data.username").value("testuser"))
                .andExpect(jsonPath("$.data.email").value("test@example.com"))
                .andExpect(jsonPath("$.data.createdAt").exists());
    }

    @Test
    @DisplayName("POST /api/users - 应该验证邮箱格式")
    void shouldValidateEmailFormat() throws Exception {
        // Given
        UserCreateRequest request = UserCreateRequest.builder()
                .username("testuser")
                .email("invalid-email")
                .password("Password123!")
                .build();

        // When & Then
        mockMvc.perform(post("/api/users")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value(400))
                .andExpect(jsonPath("$.message").value("邮箱格式不正确"));
    }

    @Test
    @DisplayName("POST /api/users - 应该验证密码强度")
    void shouldValidatePasswordStrength() throws Exception {
        // Given
        UserCreateRequest request = UserCreateRequest.builder()
                .username("testuser")
                .email("test@example.com")
                .password("weak")
                .build();

        // When & Then
        mockMvc.perform(post("/api/users")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value(400))
                .andExpect(jsonPath("$.message").value(containsString("密码")));
    }

    @Test
    @Order(2)
    @DisplayName("GET /api/users/{id} - 应该成功获取用户信息")
    void shouldGetUserById() throws Exception {
        // Given
        User user = userRepository.save(User.builder()
                .username("testuser")
                .email("test@example.com")
                .password("hashed_password")
                .build());

        // When & Then
        mockMvc.perform(get("/api/users/{id}", user.getId()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(0))
                .andExpect(jsonPath("$.data.id").value(user.getId()))
                .andExpect(jsonPath("$.data.username").value("testuser"))
                .andExpect(jsonPath("$.data.email").value("test@example.com"));
    }

    @Test
    @DisplayName("GET /api/users/{id} - 用户不存在时返回 404")
    void shouldReturn404WhenUserNotFound() throws Exception {
        // When & Then
        mockMvc.perform(get("/api/users/{id}", 999999))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("$.code").value(404))
                .andExpect(jsonPath("$.message").value("用户不存在"));
    }

    @Test
    @Order(3)
    @DisplayName("PUT /api/users/{id} - 应该成功更新用户信息")
    void shouldUpdateUserSuccessfully() throws Exception {
        // Given
        User user = userRepository.save(User.builder()
                .username("testuser")
                .email("test@example.com")
                .password("hashed_password")
                .build());

        UserUpdateRequest request = UserUpdateRequest.builder()
                .username("updated_user")
                .email("updated@example.com")
                .build();

        // When & Then
        mockMvc.perform(put("/api/users/{id}", user.getId())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(0))
                .andExpect(jsonPath("$.data.username").value("updated_user"))
                .andExpect(jsonPath("$.data.email").value("updated@example.com"));
    }

    @Test
    @Order(4)
    @DisplayName("DELETE /api/users/{id} - 应该成功删除用户")
    void shouldDeleteUserSuccessfully() throws Exception {
        // Given
        User user = userRepository.save(User.builder()
                .username("testuser")
                .email("test@example.com")
                .password("hashed_password")
                .build());

        // When & Then
        mockMvc.perform(delete("/api/users/{id}", user.getId()))
                .andExpect(status().isNoContent());

        // 验证用户已删除
        mockMvc.perform(get("/api/users/{id}", user.getId()))
                .andExpect(status().isNotFound());
    }

    @Test
    @DisplayName("GET /api/users - 应该支持分页查询")
    void shouldSupportPagination() throws Exception {
        // Given
        for (int i = 1; i <= 25; i++) {
            userRepository.save(User.builder()
                    .username("user" + i)
                    .email("user" + i + "@example.com")
                    .password("password" + i)
                    .build());
        }

        // When & Then - 查询第一页,每页10条
        mockMvc.perform(get("/api/users")
                        .param("page", "0")
                        .param("size", "10"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(0))
                .andExpect(jsonPath("$.data.content").isArray())
                .andExpect(jsonPath("$.data.content", hasSize(10)))
                .andExpect(jsonPath("$.data.totalElements").value(25))
                .andExpect(jsonPath("$.data.totalPages").value(3));
    }

    @Test
    @DisplayName("GET /api/users - 应该支持按状态筛选")
    void shouldSupportFilterByStatus() throws Exception {
        // Given
        userRepository.save(User.builder()
                .username("active_user")
                .email("active@example.com")
                .password("password")
                .status(UserStatus.ACTIVE)
                .build());
        userRepository.save(User.builder()
                .username("inactive_user")
                .email("inactive@example.com")
                .password("password")
                .status(UserStatus.INACTIVE)
                .build());

        // When & Then
        mockMvc.perform(get("/api/users")
                        .param("status", "ACTIVE"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.content", hasSize(1)))
                .andExpect(jsonPath("$.data.content[0].username").value("active_user"));
    }

    @Test
    @DisplayName("POST /api/auth/login - 应该验证登录凭证")
    @WithMockUser(roles = {})
    void shouldValidateLoginCredentials() throws Exception {
        // Given
        String password = "Password123!";
        User user = userRepository.save(User.builder()
                .username("testuser")
                .email("test@example.com")
                .password(passwordEncoder.encode(password))
                .status(UserStatus.ACTIVE)
                .build());

        LoginRequest request = LoginRequest.builder()
                .email("test@example.com")
                .password(password)
                .build();

        // When & Then
        mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(0))
                .andExpect(jsonPath("$.data.token").exists())
                .andExpect(jsonPath("$.data.user.email").value("test@example.com"));
    }

    @Test
    @DisplayName("POST /api/auth/login - 密码错误时应该返回 401")
    @WithMockUser(roles = {})
    void shouldReturn401WhenPasswordWrong() throws Exception {
        // Given
        String password = "Password123!";
        userRepository.save(User.builder()
                .username("testuser")
                .email("test@example.com")
                .password(passwordEncoder.encode(password))
                .status(UserStatus.ACTIVE)
                .build());

        LoginRequest request = LoginRequest.builder()
                .email("test@example.com")
                .password("WrongPassword!")
                .build();

        // When & Then
        mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.code").value(401))
                .andExpect(jsonPath("$.message").value("邮箱或密码错误"));
    }
}

完整业务流程测试

java 复制代码
@SpringBootTest
@Testcontainers
@AutoConfigureMockMvc
class OrderWorkflowIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine");

    @DynamicPropertySource
    static void postgresProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private ProductRepository productRepository;

    private String authToken;
    private Long userId;
    private Long productId;

    @BeforeEach
    void setUp() throws Exception {
        // 清理数据
        userRepository.deleteAll();
        productRepository.deleteAll();

        // 创建测试用户
        User user = userRepository.save(User.builder()
                .username("testuser")
                .email("test@example.com")
                .password(passwordEncoder.encode("Password123!"))
                .status(UserStatus.ACTIVE)
                .build());
        userId = user.getId();

        // 创建测试商品
        Product product = productRepository.save(Product.builder()
                .name("测试商品")
                .price(99.99)
                .stock(100)
                .status(ProductStatus.ON_SALE)
                .build());
        productId = product.getId();

        // 登录获取 token
        LoginRequest loginRequest = LoginRequest.builder()
                .email("test@example.com")
                .password("Password123!")
                .build();

        String response = mockMvc.perform(post("/api/auth/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(loginRequest)))
                .andExpect(status().isOk())
                .andReturn()
                .getResponse()
                .getContentAsString();

        JsonNode jsonNode = objectMapper.readTree(response);
        authToken = jsonNode.path("data").path("token").asText();
    }

    @Test
    @DisplayName("应该完成完整的下单流程")
    void shouldCompleteOrderWorkflow() throws Exception {
        // 1. 创建订单
        CreateOrderRequest createOrderRequest = CreateOrderRequest.builder()
                .items(List.of(
                        OrderItemRequest.builder()
                                .productId(productId)
                                .quantity(2)
                                .build()
                ))
                .build();

        String createResponse = mockMvc.perform(post("/api/orders")
                        .header("Authorization", "Bearer " + authToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(createOrderRequest)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.code").value(0))
                .andReturn()
                .getResponse()
                .getContentAsString();

        JsonNode jsonNode = objectMapper.readTree(createResponse);
        Long orderId = jsonNode.path("data").path("id").asLong();

        // 2. 查询订单详情
        mockMvc.perform(get("/api/orders/{id}", orderId)
                        .header("Authorization", "Bearer " + authToken))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.id").value(orderId))
                .andExpect(jsonPath("$.data.status").value("PENDING"))
                .andExpect(jsonPath("$.data.items").isArray())
                .andExpect(jsonPath("$.data.items", hasSize(1)));

        // 3. 支付订单
        PayOrderRequest payRequest = PayOrderRequest.builder()
                .paymentMethod(PaymentMethod.ALIPAY)
                .build();

        mockMvc.perform(post("/api/orders/{id}/pay", orderId)
                        .header("Authorization", "Bearer " + authToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(payRequest)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(0))
                .andExpect(jsonPath("$.data.status").value("PAID"));

        // 4. 查询支付后的订单
        mockMvc.perform(get("/api/orders/{id}", orderId)
                        .header("Authorization", "Bearer " + authToken))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.status").value("PAID"));

        // 5. 发货
        mockMvc.perform(post("/api/orders/{id}/ship", orderId)
                        .header("Authorization", "Bearer " + authToken))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.status").value("SHIPPED"));

        // 6. 确认收货
        mockMvc.perform(post("/api/orders/{id}/confirm", orderId)
                        .header("Authorization", "Bearer " + authToken))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.data.status").value("COMPLETED"));
    }

    @Test
    @DisplayName("应该处理库存不足的情况")
    void shouldHandleInsufficientStock() throws Exception {
        // Given - 设置库存为 1
        Product product = productRepository.findById(productId).orElseThrow();
        product.setStock(1);
        productRepository.save(product);

        // When - 尝试订购 2 件
        CreateOrderRequest request = CreateOrderRequest.builder()
                .items(List.of(
                        OrderItemRequest.builder()
                                .productId(productId)
                                .quantity(2)
                                .build()
                ))
                .build();

        // Then
        mockMvc.perform(post("/api/orders")
                        .header("Authorization", "Bearer " + authToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isBadRequest())
                .andExpect(jsonPath("$.code").value(400))
                .andExpect(jsonPath("$.message").value(containsString("库存不足")));
    }
}
复制代码
#### 3.2.2 Python FastAPI 集成测试

​```markdown
## Python FastAPI 集成测试

使用 pytest 和 TestClient 进行 API 集成测试。

### 配置示例

​```python
# conftest.py
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from testcontainers.postgres import PostgresContainer
from main import app
from database import Base, get_db

@pytest.fixture(scope="session")
def postgres_container():
    """启动 PostgreSQL 测试容器"""
    container = PostgresContainer("postgres:15-alpine")
    container.start()
    yield container
    container.stop()

@pytest.fixture(scope="function")
def db_session(postgres_container):
    """创建数据库会话"""
    engine = create_engine(postgres_container.get_connection_url())
    SessionLocal = sessionmaker(bind=engine)
    
    # 创建表结构
    Base.metadata.create_all(engine)
    
    session = SessionLocal()
    
    # 覆盖数据库依赖
    def override_get_db():
        try:
            yield session
        finally:
            session.close()
    
    app.dependency_overrides[get_db] = override_get_db
    
    yield session
    
    app.dependency_overrides.clear()
    session.close()
    Base.metadata.drop_all(engine)

@pytest.fixture
def client(db_session):
    """创建测试客户端"""
    return TestClient(app)

测试示例

python 复制代码
# test_api_users.py
import pytest
from fastapi.testclient import TestClient
from models import User, UserStatus

class TestUserAPI:
    
    @pytest.fixture
    def test_user_data(self):
        """测试用户数据"""
        return {
            "username": "testuser",
            "email": "test@example.com",
            "password": "Password123!"
        }
    
    def test_create_user_success(self, client, test_user_data):
        """测试成功创建用户"""
        # When
        response = client.post("/api/users", json=test_user_data)
        
        # Then
        assert response.status_code == 201
        data = response.json()
        assert data["code"] == 0
        assert data["data"]["username"] == test_user_data["username"]
        assert data["data"]["email"] == test_user_data["email"]
        assert "id" in data["data"]
        assert "createdAt" in data["data"]
    
    def test_create_user_invalid_email(self, client):
        """测试邮箱格式验证"""
        # Given
        invalid_data = {
            "username": "testuser",
            "email": "invalid-email",
            "password": "Password123!"
        }
        
        # When
        response = client.post("/api/users", json=invalid_data)
        
        # Then
        assert response.status_code == 422
        data = response.json()
        assert "detail" in data
    
    def test_create_user_weak_password(self, client):
        """测试密码强度验证"""
        # Given
        invalid_data = {
            "username": "testuser",
            "email": "test@example.com",
            "password": "weak"
        }
        
        # When
        response = client.post("/api/users", json=invalid_data)
        
        # Then
        assert response.status_code == 422
    
    def test_get_user_by_id_success(self, client, db_session, test_user_data):
        """测试成功获取用户信息"""
        # Given
        user = User(**test_user_data, status=UserStatus.ACTIVE)
        db_session.add(user)
        db_session.commit()
        db_session.refresh(user)
        
        # When
        response = client.get(f"/api/users/{user.id}")
        
        # Then
        assert response.status_code == 200
        data = response.json()
        assert data["code"] == 0
        assert data["data"]["id"] == user.id
        assert data["data"]["username"] == test_user_data["username"]
    
    def test_get_user_not_found(self, client):
        """测试用户不存在返回 404"""
        # When
        response = client.get("/api/users/999999")
        
        # Then
        assert response.status_code == 404
        data = response.json()
        assert data["code"] == 404
        assert "用户不存在" in data["message"]
    
    def test_update_user_success(self, client, db_session, test_user_data):
        """测试成功更新用户"""
        # Given
        user = User(**test_user_data, status=UserStatus.ACTIVE)
        db_session.add(user)
        db_session.commit()
        db_session.refresh(user)
        
        update_data = {
            "username": "updated_user",
            "email": "updated@example.com"
        }
        
        # When
        response = client.put(f"/api/users/{user.id}", json=update_data)
        
        # Then
        assert response.status_code == 200
        data = response.json()
        assert data["data"]["username"] == update_data["username"]
        assert data["data"]["email"] == update_data["email"]
    
    def test_delete_user_success(self, client, db_session, test_user_data):
        """测试成功删除用户"""
        # Given
        user = User(**test_user_data, status=UserStatus.ACTIVE)
        db_session.add(user)
        db_session.commit()
        db_session.refresh(user)
        
        # When
        response = client.delete(f"/api/users/{user.id}")
        
        # Then
        assert response.status_code == 204
        
        # 验证用户已删除
        get_response = client.get(f"/api/users/{user.id}")
        assert get_response.status_code == 404
    
    def test_list_users_with_pagination(self, client, db_session):
        """测试分页查询用户"""
        # Given - 创建 25 个用户
        for i in range(1, 26):
            user = User(
                username=f"user{i}",
                email=f"user{i}@example.com",
                password="Password123!",
                status=UserStatus.ACTIVE
            )
            db_session.add(user)
        db_session.commit()
        
        # When
        response = client.get("/api/users?page=1&size=10")
        
        # Then
        assert response.status_code == 200
        data = response.json()
        assert data["code"] == 0
        assert len(data["data"]["items"]) == 10
        assert data["data"]["total"] == 25
        assert data["data"]["pages"] == 3
    
    def test_list_users_filter_by_status(self, client, db_session):
        """测试按状态筛选用户"""
        # Given
        active_user = User(
            username="active_user",
            email="active@example.com",
            password="Password123!",
            status=UserStatus.ACTIVE
        )
        inactive_user = User(
            username="inactive_user",
            email="inactive@example.com",
            password="Password123!",
            status=UserStatus.INACTIVE
        )
        db_session.add_all([active_user, inactive_user])
        db_session.commit()
        
        # When
        response = client.get("/api/users?status=ACTIVE")
        
        # Then
        assert response.status_code == 200
        data = response.json()
        assert len(data["data"]["items"]) == 1
        assert data["data"]["items"][0]["username"] == "active_user"

认证和授权测试

python 复制代码
# test_api_auth.py
import pytest
from fastapi.testclient import TestClient
from models import User, UserStatus

class TestAuthAPI:
    
    @pytest.fixture
    def authenticated_user(self, client, db_session):
        """创建已认证的用户"""
        user = User(
            username="testuser",
            email="test@example.com",
            password="$2b$12$hashed_password",
            status=UserStatus.ACTIVE
        )
        db_session.add(user)
        db_session.commit()
        db_session.refresh(user)
        return user
    
    def test_login_success(self, client, authenticated_user):
        """测试成功登录"""
        # Given
        login_data = {
            "email": "test@example.com",
            "password": "Password123!"
        }
        
        # When
        response = client.post("/api/auth/login", json=login_data)
        
        # Then
        assert response.status_code == 200
        data = response.json()
        assert data["code"] == 0
        assert "token" in data["data"]
        assert data["data"]["user"]["email"] == "test@example.com"
    
    def test_login_wrong_password(self, client, authenticated_user):
        """测试密码错误"""
        # Given
        login_data = {
            "email": "test@example.com",
            "password": "WrongPassword!"
        }
        
        # When
        response = client.post("/api/auth/login", json=login_data)
        
        # Then
        assert response.status_code == 401
        data = response.json()
        assert data["code"] == 401
        assert "邮箱或密码错误" in data["message"]
    
    def test_access_protected_endpoint_with_token(self, client, authenticated_user):
        """测试使用 token 访问受保护端点"""
        # Given - 获取 token
        login_response = client.post("/api/auth/login", json={
            "email": "test@example.com",
            "password": "Password123!"
        })
        token = login_response.json()["data"]["token"]
        
        # When
        response = client.get(
            "/api/users/me",
            headers={"Authorization": f"Bearer {token}"}
        )
        
        # Then
        assert response.status_code == 200
        data = response.json()
        assert data["data"]["email"] == "test@example.com"
    
    def test_access_protected_endpoint_without_token(self, client):
        """测试未授权访问受保护端点"""
        # When
        response = client.get("/api/users/me")
        
        # Then
        assert response.status_code == 401
    
    def test_access_protected_endpoint_with_invalid_token(self, client):
        """测试使用无效 token 访问"""
        # When
        response = client.get(
            "/api/users/me",
            headers={"Authorization": "Bearer invalid_token"}
        )
        
        # Then
        assert response.status_code == 401
复制代码
### 3.3 服务间集成测试

​```markdown
## 服务间集成测试

使用 TestContainers 测试多个服务的协作。

### 微服务集成测试示例

​```java
@SpringBootTest
@Testcontainers
class UserServiceIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine");

    @Container
    static KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.5.0"));

    @Container
    static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
            .withExposedPorts(6379);

    @DynamicPropertySource
    static void properties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
        registry.add("spring.redis.host", redis::getHost);
        registry.add("spring.redis.port", () -> redis.getMappedPort(6379));
    }

    @Autowired
    private UserService userService;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Test
    @DisplayName("应该成功创建用户并发送事件到 Kafka")
    void shouldCreateUserAndSendEventToKafka() throws Exception {
        // Given
        UserCreateRequest request = UserCreateRequest.builder()
                .username("testuser")
                .email("test@example.com")
                .password("Password123!")
                .build();

        // 创建 Kafka 消费者监听器
        CountDownLatch latch = new CountDownLatch(1);
        AtomicReference<String> receivedMessage = new AtomicReference<>();

        kafkaTemplate.executeInTransaction(template -> {
            template.send("user-created", "key", "value");
            return null;
        });

        // When
        UserDTO user = userService.createUser(request);

        // Then - 验证用户创建成功
        assertThat(user).isNotNull();
        assertThat(user.getUsername()).isEqualTo("testuser");

        // 验证 Kafka 事件发送(需要配置消费者)
        // assertThat(receivedMessage.get()).isNotNull();
    }

    @Test
    @DisplayName("应该缓存用户信息到 Redis")
    void shouldCacheUserInRedis() {
        // Given
        User user = userRepository.save(User.builder()
                .username("testuser")
                .email("test@example.com")
                .password("hashed_password")
                .build());

        // When
        UserDTO cachedUser = userService.getUserById(user.getId());

        // Then
        assertThat(cachedUser).isNotNull();
        
        // 验证缓存
        String cachedData = redisTemplate.opsForValue().get("user:" + user.getId());
        assertThat(cachedData).isNotNull();
    }

    @Test
    @DisplayName("应该处理服务间超时")
    void shouldHandleServiceTimeout() {
        // Given
        UserCreateRequest request = UserCreateRequest.builder()
                .username("testuser")
                .email("test@example.com")
                .password("Password123!")
                .build();

        // Mock 外部服务超时
        // when(externalService.call()).thenThrow(new TimeoutException());

        // When & Then
        Assertions.assertThrows(ServiceTimeoutException.class, () -> {
            userService.createUserWithExternalService(request);
        });
    }
}

消息队列集成测试

java 复制代码
@SpringBootTest
@Testcontainers
class OrderServiceKafkaIntegrationTest {

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine");

    @Container
    static KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.5.0"));

    @DynamicPropertySource
    static void properties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
    }

    @Autowired
    private OrderService orderService;

    @Autowired
    private KafkaTemplate<String, OrderEvent> kafkaTemplate;

    @Autowired
    private ConsumerFactory<String, OrderEvent> consumerFactory;

    @Test
    @DisplayName("应该发送订单创建事件到 Kafka")
    void shouldSendOrderCreatedEventToKafka() throws Exception {
        // Given
        CreateOrderRequest request = CreateOrderRequest.builder()
                .userId(1L)
                .items(List.of(
                        OrderItemRequest.builder()
                                .productId(1L)
                                .quantity(2)
                                .build()
                ))
                .build();

        // 创建消费者监听
        CountDownLatch latch = new CountDownLatch(1);
        AtomicReference<OrderEvent> receivedEvent = new AtomicReference<>();

        ContainerProperties containerProps = new ContainerProperties("order-created");
        containerProps.setGroupId("test-group");
        containerProps.setMessageListener((MessageListener<String, OrderEvent>) record -> {
            receivedEvent.set(record.value());
            latch.countDown();
        });

        KafkaMessageListenerContainer<String, OrderEvent> container =
                new KafkaMessageListenerContainer<>(consumerFactory, containerProps);
        container.start();

        try {
            // When
            orderService.createOrder(request);

            // Then - 等待消息接收
            boolean received = latch.await(5, TimeUnit.SECONDS);
            assertThat(received).isTrue();
            assertThat(receivedEvent.get()).isNotNull();
            assertThat(receivedEvent.get().getType()).isEqualTo(OrderEventType.CREATED);
        } finally {
            container.stop();
        }
    }

    @Test
    @DisplayName("应该从 Kafka 接收支付成功事件并更新订单状态")
    void shouldReceivePaymentSuccessEventAndUpdateOrderStatus() throws Exception {
        // Given
        Order order = orderRepository.save(Order.builder()
                .userId(1L)
                .orderNumber("ORD123456")
                .status(OrderStatus.PENDING)
                .build());

        PaymentSuccessEvent event = PaymentSuccessEvent.builder()
                .orderId(order.getId())
                .transactionId("TXN123456")
                .amount(new BigDecimal("99.99"))
                .build();

        // 发送支付成功事件
        kafkaTemplate.send("payment-success", event);

        // Then - 验证订单状态更新
        await().atMost(5, TimeUnit.SECONDS)
                .untilAsserted(() -> {
                    Order updatedOrder = orderRepository.findById(order.getId()).orElseThrow();
                    assertThat(updatedOrder.getStatus()).isEqualTo(OrderStatus.PAID);
                });
    }
}
复制代码
## 四、在 Claude Code 中使用集成测试 Skill

### 4.1 自动触发(推荐)

直接在 Claude Code 中提出需求:

帮我为用户服务写集成测试

复制代码

为订单 API 生成完整的集成测试用例

复制代码
Claude 会自动:
1. 识别测试类型(数据库/API/服务间)
2. 配置测试环境(TestContainers、Mock服务器)
3. 生成符合规范的测试代码
4. 包含事务管理和测试隔离

### 4.2 手动触发

/integration-test

复制代码
然后描述具体需求:

/integration-test 为商品服务写数据库集成测试,包括CRUD和复杂查询

复制代码
### 4.3 测试代码优化

如果已有测试代码但质量不佳,可以让 Claude 优化:

优化这个集成测试,提高稳定性和可维护性

复制代码
## 五、实战案例

### 5.1 场景一:为用户模块生成数据库集成测试

**需求描述**:

我需要为用户模块写数据库集成测试,包括:

实体类:

java 复制代码
@Entity
@Table(name = "users")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String username;
    
    @Column(nullable = false, unique = true)
    private String email;
    
    @Column(nullable = false)
    private String password;
    
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private UserStatus status = UserStatus.ACTIVE;
    
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
    
    @Version
    private Integer version;
}

Repository:

java 复制代码
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
    Optional<User> findByEmail(String email);
    List<User> findByStatus(UserStatus status);
    Page<User> findByStatus(UserStatus status, Pageable pageable);
    boolean existsByUsername(String username);
    boolean existsByEmail(String email);
}

需要测试:

  1. 基础 CRUD 操作

  2. 唯一性约束

  3. 复杂查询(分页、筛选)

  4. 事务回滚

  5. 乐观锁

    Claude 生成的测试代码

    java 复制代码
    @SpringBootTest
    @Testcontainers
    @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
    class UserRepositoryIntegrationTest {
    
        @Container
        static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
                .withDatabaseName("testdb")
                .withUsername("testuser")
                .withPassword("testpass");
    
        @DynamicPropertySource
        static void postgresProperties(DynamicPropertyRegistry registry) {
            registry.add("spring.datasource.url", postgres::getJdbcUrl);
            registry.add("spring.datasource.username", postgres::getUsername);
            registry.add("spring.datasource.password", postgres::getPassword);
        }
    
        @Autowired
        private UserRepository userRepository;
    
        private User testUser;
    
        @BeforeEach
        void setUp() {
            userRepository.deleteAll();
            testUser = User.builder()
                    .username("testuser")
                    .email("test@example.com")
                    .password("password123")
                    .status(UserStatus.ACTIVE)
                    .build();
        }
    
        @Test
        @Order(1)
        @DisplayName("应该成功保存用户")
        void shouldSaveUserSuccessfully() {
            // When
            User savedUser = userRepository.save(testUser);
    
            // Then
            assertThat(savedUser).isNotNull();
            assertThat(savedUser.getId()).isNotNull();
            assertThat(savedUser.getUsername()).isEqualTo("testuser");
            assertThat(savedUser.getEmail()).isEqualTo("test@example.com");
            assertThat(savedUser.getCreatedAt()).isNotNull();
            assertThat(savedUser.getVersion()).isEqualTo(0);
        }
    
        @Test
        @Order(2)
        @DisplayName("应该通过ID查找用户")
        void shouldFindUserById() {
            // Given
            User savedUser = userRepository.save(testUser);
    
            // When
            Optional<User> foundUser = userRepository.findById(savedUser.getId());
    
            // Then
            assertThat(foundUser).isPresent();
            assertThat(foundUser.get().getUsername()).isEqualTo("testuser");
            assertThat(foundUser.get().getEmail()).isEqualTo("test@example.com");
        }
    
        @Test
        @Order(3)
        @DisplayName("应该通过用户名查找用户")
        void shouldFindByUsername() {
            // Given
            userRepository.save(testUser);
    
            // When
            Optional<User> foundUser = userRepository.findByUsername("testuser");
    
            // Then
            assertThat(foundUser).isPresent();
            assertThat(foundUser.get().getEmail()).isEqualTo("test@example.com");
        }
    
        @Test
        @Order(4)
        @DisplayName("应该通过邮箱查找用户")
        void shouldFindByEmail() {
            // Given
            userRepository.save(testUser);
    
            // When
            Optional<User> foundUser = userRepository.findByEmail("test@example.com");
    
            // Then
            assertThat(foundUser).isPresent();
            assertThat(foundUser.get().getUsername()).isEqualTo("testuser");
        }
    
        @Test
        @Order(5)
        @DisplayName("应该更新用户信息")
        void shouldUpdateUser() {
            // Given
            User savedUser = userRepository.save(testUser);
            savedUser.setEmail("updated@example.com");
    
            // When
            User updatedUser = userRepository.save(savedUser);
    
            // Then
            assertThat(updatedUser.getEmail()).isEqualTo("updated@example.com");
            assertThat(updatedUser.getUpdatedAt()).isNotNull();
            assertThat(updatedUser.getVersion()).isEqualTo(1);
        }
    
        @Test
        @Order(6)
        @DisplayName("应该删除用户")
        void shouldDeleteUser() {
            // Given
            User savedUser = userRepository.save(testUser);
    
            // When
            userRepository.deleteById(savedUser.getId());
    
            // Then
            Optional<User> deletedUser = userRepository.findById(savedUser.getId());
            assertThat(deletedUser).isEmpty();
        }
    
        @Test
        @DisplayName("应该验证用户名唯一性约束")
        void shouldValidateUsernameUniqueness() {
            // Given
            User user1 = User.builder()
                    .username("sameuser")
                    .email("user1@example.com")
                    .password("password123")
                    .build();
            User user2 = User.builder()
                    .username("sameuser")
                    .email("user2@example.com")
                    .password("password456")
                    .build();
    
            // When & Then
            userRepository.save(user1);
            Assertions.assertThrows(DataIntegrityViolationException.class, () -> {
                userRepository.save(user2);
            });
        }
    
        @Test
        @DisplayName("应该验证邮箱唯一性约束")
        void shouldValidateEmailUniqueness() {
            // Given
            User user1 = User.builder()
                    .username("user1")
                    .email("same@example.com")
                    .password("password123")
                    .build();
            User user2 = User.builder()
                    .username("user2")
                    .email("same@example.com")
                    .password("password456")
                    .build();
    
            // When & Then
            userRepository.save(user1);
            Assertions.assertThrows(DataIntegrityViolationException.class, () -> {
                userRepository.save(user2);
            });
        }
    
        @Test
        @DisplayName("应该检查用户名是否存在")
        void shouldCheckUsernameExists() {
            // Given
            userRepository.save(testUser);
    
            // When & Then
            assertThat(userRepository.existsByUsername("testuser")).isTrue();
            assertThat(userRepository.existsByUsername("nonexistent")).isFalse();
        }
    
        @Test
        @DisplayName("应该检查邮箱是否存在")
        void shouldCheckEmailExists() {
            // Given
            userRepository.save(testUser);
    
            // When & Then
            assertThat(userRepository.existsByEmail("test@example.com")).isTrue();
            assertThat(userRepository.existsByEmail("nonexistent@example.com")).isFalse();
        }
    
        @Test
        @DisplayName("应该按状态查找用户")
        void shouldFindByStatus() {
            // Given
            User activeUser = User.builder()
                    .username("activeuser")
                    .email("active@example.com")
                    .password("password123")
                    .status(UserStatus.ACTIVE)
                    .build();
            User inactiveUser = User.builder()
                    .username("inactiveuser")
                    .email("inactive@example.com")
                    .password("password456")
                    .status(UserStatus.INACTIVE)
                    .build();
            userRepository.saveAll(List.of(activeUser, inactiveUser));
    
            // When
            List<User> activeUsers = userRepository.findByStatus(UserStatus.ACTIVE);
            List<User> inactiveUsers = userRepository.findByStatus(UserStatus.INACTIVE);
    
            // Then
            assertThat(activeUsers).hasSize(1);
            assertThat(activeUsers.get(0).getUsername()).isEqualTo("activeuser");
            
            assertThat(inactiveUsers).hasSize(1);
            assertThat(inactiveUsers.get(0).getUsername()).isEqualTo("inactiveuser");
        }
    
        @Test
        @DisplayName("应该支持分页查询")
        void shouldSupportPagination() {
            // Given - 创建 20 个用户
            List<User> users = new ArrayList<>();
            for (int i = 1; i <= 20; i++) {
                users.add(User.builder()
                        .username("user" + i)
                        .email("user" + i + "@example.com")
                        .password("password" + i)
                        .status(i % 2 == 0 ? UserStatus.ACTIVE : UserStatus.INACTIVE)
                        .build());
            }
            userRepository.saveAll(users);
    
            // When - 查询第一页,每页 10 条
            Pageable pageable = PageRequest.of(0, 10);
            Page<User> userPage = userRepository.findByStatus(UserStatus.ACTIVE, pageable);
    
            // Then
            assertThat(userPage.getTotalElements()).isEqualTo(10); // 活跃用户只有 10 个
            assertThat(userPage.getTotalPages()).isEqualTo(1);
            assertThat(userPage.getContent()).hasSize(10);
            assertThat(userPage.isFirst()).isTrue();
            assertThat(userPage.isLast()).isTrue();
        }
    
        @Test
        @Transactional
        @DisplayName("应该测试事务回滚")
        void shouldTestTransactionRollback() {
            // Given
            User savedUser = userRepository.save(testUser);
            Long originalVersion = savedUser.getVersion();
    
            // When
            Assertions.assertThrows(RuntimeException.class, () -> {
                savedUser.setEmail("updated@example.com");
                userRepository.save(savedUser);
                throw new RuntimeException("模拟异常");
            });
    
            // Then - 事务应该回滚
            User recoveredUser = userRepository.findById(savedUser.getId()).orElseThrow();
            assertThat(recoveredUser.getEmail()).isEqualTo("test@example.com");
            assertThat(recoveredUser.getVersion()).isEqualTo(originalVersion);
        }
    
        @Test
        @DisplayName("应该测试乐观锁")
        void shouldTestOptimisticLock() {
            // Given
            User user = userRepository.save(testUser);
    
            // When - 模拟并发更新
            User user1 = userRepository.findById(user.getId()).orElseThrow();
            User user2 = userRepository.findById(user.getId()).orElseThrow();
    
            user1.setEmail("user1@example.com");
            userRepository.save(user1);
    
            user2.setEmail("user2@example.com");
            Assertions.assertThrows(ObjectOptimisticLockingFailureException.class, () -> {
                userRepository.save(user2);
            });
    
            // Then - 只有第一个更新成功
            User finalUser = userRepository.findById(user.getId()).orElseThrow();
            assertThat(finalUser.getEmail()).isEqualTo("user1@example.com");
            assertThat(finalUser.getVersion()).isEqualTo(1);
        }
    
        @Test
        @DisplayName("应该批量操作")
        void shouldBatchOperation() {
            // Given
            List<User> users = new ArrayList<>();
            for (int i = 1; i <= 100; i++) {
                users.add(User.builder()
                        .username("user" + i)
                        .email("user" + i + "@example.com")
                        .password("password" + i)
                        .build());
            }
    
            // When
            List<User> savedUsers = userRepository.saveAll(users);
    
            // Then
            assertThat(savedUsers).hasSize(100);
            assertThat(userRepository.count()).isEqualTo(100);
        }
    }

六、测试数据管理

6.1 测试数据隔离

markdown 复制代码
## 测试数据隔离策略

### 使用 @Transactional 自动回滚

​```java
@SpringBootTest
@Transactional
class UserServiceTest {
    
    @Autowired
    private UserService userService;
    
    @Test
    void shouldCreateUser() {
        // 测试完成后自动回滚,不污染数据库
        userService.createUser(...);
    }
}

手动清理数据

java 复制代码
@SpringBootTest
class UserServiceTest {
    
    @Autowired
    private UserRepository userRepository;
    
    @BeforeEach
    void setUp() {
        // 每个测试前清理数据
        userRepository.deleteAll();
    }
    
    @AfterEach
    void tearDown() {
        // 每个测试后清理数据
        userRepository.deleteAll();
    }
}

使用 Testcontainers 独立容器

每个测试套件使用独立的 Docker 容器,完全隔离测试环境。

复制代码
### 6.2 测试数据构建

​```markdown
## 测试数据构建器

### 使用 Builder 模式

​```java
public class UserTestDataBuilder {
    private Long id;
    private String username = "testuser";
    private String email = "test@example.com";
    private String password = "password123";
    private UserStatus status = UserStatus.ACTIVE;
    
    public static UserTestDataBuilder aUser() {
        return new UserTestDataBuilder();
    }
    
    public UserTestDataBuilder withUsername(String username) {
        this.username = username;
        return this;
    }
    
    public UserTestDataBuilder withEmail(String email) {
        this.email = email;
        return this;
    }
    
    public UserTestDataBuilder withStatus(UserStatus status) {
        this.status = status;
        return this;
    }
    
    public User build() {
        return User.builder()
                .id(id)
                .username(username)
                .email(email)
                .password(password)
                .status(status)
                .build();
    }
}

// 使用示例
@Test
void shouldCreateUser() {
    User user = UserTestDataBuilder.aUser()
            .withUsername("customuser")
            .withEmail("custom@example.com")
            .withStatus(UserStatus.INACTIVE)
            .build();
    
    userRepository.save(user);
}


## 七、常见问题与解决方案

### 7.1 测试不稳定

| 问题 | 原因 | 解决方案 |
|------|------|---------|
| 测试偶发性失败 | 测试间数据污染 | 使用 @Transactional 或清理数据 |
| 测试执行慢 | 没有使用并行执行 | 配置并行执行,使用 TestContainers |
| Mock 不生效 | Mock 配置错误 | 检查 @MockBean 和 @SpyBean 配置 |
| 数据库连接失败 | 容器启动超时 | 增加 TestContainers 启动超时时间 |
| 事务回滚失败 | 事务传播配置错误 | 检查 @Transactional 注解配置 |

### 7.2 性能优化

​```markdown
## 集成测试性能优化

### 1. 使用 TestContainers 容器复用

​```java
@Testcontainers
class Test {
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15-alpine")
            .withReuse(true); // 容器复用
}

2. 并行执行测试

java 复制代码
@SpringBootTest
@Execution(ExecutionMode.CONCURRENT) // JUnit 5 并行执行
class ParallelTest {
    // 测试用例
}

3. 使用嵌入式数据库

properties 复制代码
# application-test.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.hibernate.ddl-auto=create-drop

4. 延迟加载依赖

java 复制代码
@TestPropertySource(properties = {
    "spring.autoconfigure.exclude=com.example.SomeAutoConfiguration"
})
复制代码
## 八、总结

### 核心价值

通过创建集成测试自动化 Skill,我们获得了:

| 维度 | 提升 |
|------|------|
| **测试质量** | 真实环境测试,发现隐藏 Bug |
| **开发效率** | 测试编写时间减少 60% 以上 |
| **系统稳定性** | 提前发现集成问题,降低线上故障 |
| **重构信心** | 有集成测试保护,敢于重构 |
| **团队协作** | 统一测试规范,降低沟通成本 |

### 关键要点

1. **测试层次**:数据库集成 + API 集成 + 服务间集成 + 缓存集成 + 消息队列集成
2. **真实环境**:使用 TestContainers 提供真实的测试环境
3. **数据隔离**:确保测试间不互相影响
4. **事务管理**:合理使用事务回滚保持数据干净
5. **性能优化**:容器复用、并行执行、延迟加载

### 下一步建议

1. **创建你的第一个集成测试 Skill**:从当前项目的某个模块开始
2. **配置 TestContainers**:建立标准的测试容器环境
3. **建立测试数据管理**:制定测试数据构建和清理策略
4. **配置 CI/CD**:将集成测试集成到持续集成流程
5. **监控测试覆盖率**:追踪集成测试的覆盖率

### 最后的话

集成测试是保障系统稳定性的最后一道防线。通过 Claude Skills,我们可以大幅降低集成测试的编写难度,让每个后端开发者都能轻松写出高质量的集成测试代码。

记住:**没有集成测试的系统,就像没有验收标准的工程,质量无法保障。**

---

## 附录:集成测试完整模板

完整的集成测试模板已开源,包含:
- ✅ 数据库集成测试模板(Spring Boot/Python)
- ✅ API 集成测试模板(REST/GraphQL)
- ✅ 服务间集成测试模板
- ✅ 缓存集成测试模板
- ✅ 消息队列集成测试模板
- ✅ TestContainers 配置模板

你可以直接使用或根据项目需求进行定制。

## 参考资源

- [Spring Boot Test 官方文档](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing)
- [TestContainers 官方文档](https://www.testcontainers.org/)
- [Jest 官方文档](https://jestjs.io/)
- [pytest 官方文档](https://docs.pytest.org/)
- [WireMock 官方文档](http://wiremock.org/)

---

**作者**:猿来如此呀
**发布时间**:2026-01-30
**阅读时间**:约 25 分钟
**难度等级**:中高级
相关推荐
5 小时前
java关于内部类
java·开发语言
好好沉淀5 小时前
Java 项目中的 .idea 与 target 文件夹
java·开发语言·intellij-idea
Wang201220135 小时前
芯片serdes phy vth下阈值过低,线缆干扰会识别成oob如何解决
集成测试
gusijin5 小时前
解决idea启动报错java: OutOfMemoryError: insufficient memory
java·ide·intellij-idea
To Be Clean Coder5 小时前
【Spring源码】createBean如何寻找构造器(二)——单参数构造器的场景
java·后端·spring
吨~吨~吨~5 小时前
解决 IntelliJ IDEA 运行时“命令行过长”问题:使用 JAR
java·ide·intellij-idea
你才是臭弟弟5 小时前
SpringBoot 集成MinIo(根据上传文件.后缀自动归类)
java·spring boot·后端
短剑重铸之日5 小时前
《设计模式》第二篇:单例模式
java·单例模式·设计模式·懒汉式·恶汉式
码农水水5 小时前
得物Java面试被问:消息队列的死信队列和重试机制
java·开发语言·jvm·数据结构·机器学习·面试·职场和发展
summer_du5 小时前
IDEA插件下载缓慢,如何解决?
java·ide·intellij-idea