集成测试自动化:用 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);
}
需要测试:
-
基础 CRUD 操作
-
唯一性约束
-
复杂查询(分页、筛选)
-
事务回滚
-
乐观锁
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 分钟
**难度等级**:中高级