集成测试
Testcontainers、真实 MySQL/Redis。
集成测试
这一课你将能做到
- 说出哪些问题 mock 测不到(SQL、唯一索引、事务、迁移),并归类「可 mock / 必集成 / 不必每次跑」三张清单
- 用 Testcontainers +
@ServiceConnection写出对真 MySQL 的插入-查询往返用例,包括唯一索引冲突 - 用
*Test/*IT命名约定与 Failsafe 插件把快慢测分开跑
为什么要学
单元测试把 Mapper mock 掉之后,仍会出现「单测全绿、线上炸库」:SQL 写错、唯一索引没建、事务边界不对、字段映射错位。人话:替身演员台词背得再熟,也不代表真舞台的布景能承重。
集成测试用真实或容器化的 MySQL / Redis,验证多组件一起工作。它贵(秒级起步),所以要克制:只覆盖 mock 无法替代的路径。CodeForge 生产用 MySQL 8 + Redis,集成测试就该连同样的东西,而不是找个内存库自欺欺人。
先建立图像
单元: Service ──mock──► Mapper(假,你编的答案)
集成: Service ──真──► Mapper ──真──► MySQL(Testcontainers 容器)
成本: 启容器 + 建表 + 插入 + 查询 ≫ 纯 Mockito
收益: 抓住「SQL / 事务 / 装配 / 迁移」类缺陷
| 方式 | 优点 | 注意 |
|---|---|---|
| Testcontainers | 本地与 CI 一致,版本可钉死 | 需要 Docker |
CI services: 起 MySQL | workflow 原生 | 本地要另备库 |
| 内存库(H2 等) | 快 | 方言差异风险大 |
命名约定:*Test.java 快测(Surefire 默认跑);*IT.java 集成测(Failsafe 在 verify 阶段跑)。
跟着做
步骤 1:先列三张清单,再写代码
打开 backend 的 Mapper / 建表脚本,把场景归类:
- 可 mock:纯 Service 分支、DTO 拼装(现有
*ServiceTest全在此列,保留,不要改成打真库) - 必须集成:
userMapper.insert与唯一索引、事务回滚、复杂查询 - 不必每次 PR:Redis + 判题全链路重型场景(可夜间跑)
步骤 2:引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<scope>test</scope>
</dependency>
步骤 3:写第一个真 MySQL 往返用例
Spring Boot 3.1+ 提供 @ServiceConnection,容器连接信息自动注入数据源,比手写 @DynamicPropertySource 更现行:
import com.codeforge.entity.User;
import com.codeforge.mapper.UserMapper;
import com.mybatisflex.core.query.QueryWrapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.dao.DuplicateKeyException;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.assertj.core.api.Assertions.*;
@SpringBootTest
@Testcontainers
class UserMapperIT {
@Container
@ServiceConnection
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
@Autowired
UserMapper userMapper;
private User newUser(String name) {
User user = new User();
user.setUsername(name);
user.setEmail(name + "@example.com");
user.setPasswordHash("$2a$10$fakehashfakehashfakehash");
return user;
}
@Test
void 用户写入后可按用户名查回() {
String name = "t_" + System.nanoTime(); // 每次唯一,防脏数据
userMapper.insert(newUser(name));
User loaded = userMapper.selectOneByQuery(
QueryWrapper.create().where(User::getUsername).eq(name));
assertThat(loaded).isNotNull();
assertThat(loaded.getEmail()).isEqualTo(name + "@example.com");
assertThat(loaded.getPasswordHash()).isNotBlank();
}
@Test
void 用户名唯一索引真实生效() {
String name = "u_" + System.nanoTime();
userMapper.insert(newUser(name));
// 同名再插:mock 永远证不了这一条,只有真库能
assertThatThrownBy(() -> userMapper.insert(newUser(name)))
.isInstanceOf(DuplicateKeyException.class);
}
}