一、单元测试
1.1 单元测试基础概念
单元测试是针对软件中最小可测试单元(通常是方法或类)进行检查和验证的过程。在Java后端开发中,我们主要测试Service层的业务逻辑。
为什么需要单元测试?
早期发现代码缺陷
确保代码修改不会破坏现有功能
作为代码文档,展示如何使用被测试代码
促进更好的代码设计(可测试的代码通常结构更好)
常见测试类结构是怎样的?
是的,标准做法是每个业务类(如 UserService
),对应一个测试类:
src/
├─ main/
│ └─ java/com/example/service/UserService.java
└─ test/
└─ java/com/example/service/UserServiceTest.java
在测试类中,你:
创建
@Mock
依赖(如 Mapper)创建
@InjectMocks
的 Service编写
@Test
方法,使用断言验证行为
1.2 单元测试完整依赖
如果你使用的是 Spring Boot,可以选择直接引入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
这个 starter 已经集成了 JUnit5 + Mockito + AssertJ 等测试依赖,适合大部分项目。
<!-- JUnit5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.3</version>
<scope>test</scope>
</dependency>
<!-- Mockito 核心 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.8.0</version>
<scope>test</scope>
</dependency>
<!-- Mockito + JUnit5 集成支持(必须加上) -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.8.0</version>
<scope>test</scope>
</dependency>
mockito-junit-jupiter
这个依赖是为了让你可以使用@ExtendWith(MockitoExtension.class)
与 JUnit5 配合。 如果不使用这种方式就要通过下面这种方式告诉 Mockito“请扫描当前类中的 @Mock、@InjectMocks 注解,创建 Mock 对象并注入”。
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
1.3 JUnit5 基本用法
1.3.1 基本注解
@Test
: 标记一个方法为测试方法@BeforeEach
: 在每个测试方法前执行@AfterEach
: 在每个测试方法后执行@BeforeAll
: 在所有测试方法前执行(静态方法)@AfterAll
: 在所有测试方法后执行(静态方法)@DisplayName
: 为测试类或方法指定显示名称
(1)@BeforeAll
和 @AfterAll
执行时机:
@BeforeAll
:在整个测试类的所有测试方法执行之前运行一次@AfterAll
:在整个测试类的所有测试方法执行之后运行一次
静态方法要求:这两个注解标记的方法必须是static
,因为它们在类级别执行,不依赖于任何测试实例
典型用途:
class DatabaseTest {
static Connection connection;
@BeforeAll
static void initDatabase() {
connection = Database.connect(); // 所有测试共享的昂贵资源
}
@AfterAll
static void closeDatabase() {
connection.close(); // 所有测试完成后清理资源
}
@Test void testQuery1() { /* 使用connection */ }
@Test void testQuery2() { /* 使用connection */ }
}
(2)@BeforeEach
和 @AfterEach
执行时机:
@BeforeEach
:在每个测试方法执行之前运行@AfterEach
:在每个测试方法执行之后运行
非静态方法:不需要static
修饰
典型用途:
class CalculatorTest {
Calculator calculator;
@BeforeEach
void init() {
calculator = new Calculator(); // 每个测试前创建新实例
}
@AfterEach
void cleanup() {
calculator.reset(); // 每个测试后清理状态
}
@Test void testAdd() { /* 使用新实例 */ }
@Test void testSubtract() { /* 使用新实例 */ }
}
1.3.2 常用断言方法
定义:
断言(assert)是用于判断实际结果是否符合预期结果的“测试判断语句”。
如果断言成功:测试通过 ✅
如果断言失败:测试失败 ❌(会抛出 AssertionFailedError)
为什么断言很重要?
没有断言,只是“运行代码”;
有了断言,才能“验证结果是否正确”。
方法 | 用途 | 示例 |
---|---|---|
assertEquals |
验证预期值=实际值 | assertEquals(10, result) |
assertTrue |
验证条件为真 | assertTrue(list.isEmpty()) |
assertFalse |
验证条件为假 | assertFalse(user.isActive()) |
assertNull |
验证对象为null | assertNull(error) |
assertNotNull |
验证对象非null | assertNotNull(response) |
assertThrows |
验证是否抛出指定异常 | assertThrows(IllegalArgumentException.class, () → service.method(null)) |
assertAll |
分组执行多个断言 | assertAll("用户属性", () → assertEquals("John", user.name), () → assertEquals(30, user.age)) |
1.3.3 示例:测试工具类方法
class DateUtilsTest {
@Test
@DisplayName("测试日期格式化")
void testFormatDate() {
LocalDate date = LocalDate.of(2023, 5, 15);
String expected = "2023-05-15";
assertEquals(expected, DateUtils.formatDate(date));
}
@Test
@DisplayName("测试解析日期")
void testParseDate() {
String dateStr = "2023-05-15";
LocalDate expected = LocalDate.of(2023, 5, 15);
assertEquals(expected, DateUtils.parseDate(dateStr));
}
@Test
@DisplayName("测试非法日期格式")
void testInvalidDateFormat() {
assertThrows(DateTimeParseException.class, () -> {
DateUtils.parseDate("2023/05/15");
});
}
@Test
@DisplayName("测试计算日期差")
void testDaysBetween() {
LocalDate start = LocalDate.of(2023, 5, 10);
LocalDate end = LocalDate.of(2023, 5, 15);
assertEquals(5, DateUtils.daysBetween(start, end));
}
}
class ValidationUtilsTest {
@Test
@DisplayName("测试邮箱验证")
void testEmailValidation() {
assertAll("邮箱验证测试",
() -> assertTrue(ValidationUtils.isValidEmail("test@example.com")),
() -> assertTrue(ValidationUtils.isValidEmail("user.name+tag@domain.co")),
() -> assertFalse(ValidationUtils.isValidEmail("invalid.email")),
() -> assertFalse(ValidationUtils.isValidEmail("user@.com")),
() -> assertFalse(ValidationUtils.isValidEmail(null))
);
}
@Test
@DisplayName("测试手机号验证")
void testPhoneValidation() {
assertAll("手机号验证测试",
() -> assertTrue(ValidationUtils.isValidPhone("13812345678")),
() -> assertFalse(ValidationUtils.isValidPhone("12345678")),
() -> assertFalse(ValidationUtils.isValidPhone("138123456789")),
() -> assertFalse(ValidationUtils.isValidPhone("abc12345678"))
);
}
}
1.4 Mockito 使用
Mockito是一个流行的Mock框架,用于创建和配置模拟对象,特别适合测试Service层时模拟Repository/DAO层。
核心概念
Mock对象:模拟真实对象的替代品,可以预设行为和返回值
Spy对象:部分模拟,对未存根的方法调用真实方法
1.4.1 常用注解
@Mock
: 创建模拟对象@InjectMocks
: 创建实例并自动注入@Mock或@Spy字段@Spy
: 创建spy对象
一个测试类中是否只能有一个 @InjectMocks
?
不是只能有一个,但你必须明确每个要注入的对象,并且不能存在注入冲突。
🟡 多个 @InjectMocks
是可以的,只要不冲突!
@InjectMocks
private UserServiceImpl userService;
@InjectMocks
private OrderServiceImpl orderService;
这种写法是允许的,只要这些 service 所依赖的 mock 字段(
@Mock
)都能被唯一地识别出来。
❌ 什么时候会冲突?
如果两个 @InjectMocks
修饰的类依赖的是同一个 @Mock
,但你没有明确区分,那么 Mockito 就无法判断该把 mock 注入给哪个对象,会报错或行为混乱。
1.4.2 常用方法
表示
when(...)
里的内容 必须是“对 mock 对象的方法调用”而不能是静态方法。如果想在中调用一个静态方法,必须按照以下方式:
你需要用
try (MockedStatic<...> ignored = ...)
的方式包装调用:✅ 示例:mock
SecurityContextUtil.getUserId()
import org.mockito.MockedStatic; import org.mockito.Mockito; @Test public void testAddFavorite() { Long userId = 1L; Long resourceId = 1L; // Mock 静态方法 try (MockedStatic<SecurityContextUtil> mockedStatic = Mockito.mockStatic(SecurityContextUtil.class)) { mockedStatic.when(SecurityContextUtil::getUserId).thenReturn(userId); when(resourceService.checkResourceExist(resourceId)).thenReturn(true); when(redisTemplate.execute(any(), any(), any(), any())).thenReturn(1L); Boolean result = resourceFavoriteService.addFavorite(resourceId); assertTrue(result); } }
⚠ 注意事项
mockStatic(...)
返回的是MockedStatic<T>
类型,必须用 try-with-resources 包裹,否则 mock 不生效;静态方法的 mock 是线程隔离的,只在 try 块中有效;
不支持 mock final 类或 native 方法;
IDE(如 IntelliJ IDEA)有时不能正确识别静态 mock,需要你加
mockito-inline
依赖;
(1)when(...).thenReturn(...)
—— 模拟方法返回值
作用:告诉 Mockito:“当这个 mock 对象调用某个方法时,请返回我指定的值”。
默认情况下,mock 对象的方法如果没有用
when(...).thenReturn(...)
指定行为,会返回该方法返回类型的默认值:
方法返回类型 默认返回值 boolean
false
int
0
Object
null
List
空列表/null
示例:
when(userMapper.selectById(1L)).thenReturn(new User(1L, "张三"));
✅ 表示:如果测试代码中调用
userMapper.selectById(1L)
,就返回一个张三对象,而不会真的访问数据库。在你自定义的被@Test注解的方法中,在调用包含userMapper.selectById(1L)的service层的方法之前使用这行代码,它会在你调用这个service层的方法中的userMapper.selectById(1L)时进行拦截返回对应的值。
@ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserMapper userMapper; @InjectMocks private UserService userService; @Test void testGetUserName() { // 1. 准备阶段:告诉 mock 对象该如何响应 when(userMapper.selectById(1L)) .thenReturn(new User(1L, "张三")); // 2. 执行阶段:调用你要测试的业务方法 String name = userService.getUserName(1L); // 3. 断言阶段:验证返回值 assertEquals("张三", name); // 4.(可选)交互验证:确认底层 mapper 方法被调用 verify(userMapper).selectById(1L); } }
你在
when()
中指定的参数,要与被测代码调用 mock 方法时传入的参数值完全一致,否则不会命中,返回默认值(如 null、false、0)。想匹配“任意参数”?用参数匹配器:
anyXXX()
Mockito 提供了参数匹配器,比如:
匹配器方法 说明 any()
匹配任意类型对象 anyLong()
匹配任意 long 值 anyInt()
匹配任意 int 值 anyString()
匹配任意字符串
🔁 多次调用返回不同结果:
when(service.getValue()).thenReturn("A").thenReturn("B");
第一次调用返回 A,第二次返回 B。
(2)verify(mock).method()
—— 验证方法是否被调用
作用:测试代码是否“真的”调用了某个方法。
通常用于验证逻辑流程是否正确,如删除用户时是否调用了数据库删除方法。
示例:
userService.deleteUser(1L);
verify(userMapper).deleteById(1L); // 检查是否调用了 deleteById
(3)verify(mock, times(n)).method()
—— 验证方法被调用 n 次
作用:在一些场景中我们期望某个方法被调用多次或指定次数,这时用 times(n)
。
示例:
userService.batchDelete(Arrays.asList(1L, 2L, 3L));
verify(userMapper, times(3)).deleteById(anyLong());
检查
userMapper.deleteById
是否被调用了 3 次。
(4)any()
, anyString()
, anyInt()
等参数匹配器
作用:用于设置/验证时对“任意参数”的匹配,不用写死具体参数。
避免你写死具体值,测试更灵活、健壮。
示例1:模拟返回值
when(userMapper.selectById(anyLong())).thenReturn(new User(1L, "默认用户"));
表示无论你传什么 long 类型参数,都返回这个默认用户。
示例2:验证方法调用
verify(userMapper).selectById(anyLong());
表示只要这个方法被调用,不管参数是什么,就算验证通过。
小总结:这些方法的配合使用
场景 | 使用方法 |
---|---|
设置 mock 返回值 | when(...).thenReturn(...) |
验证方法是否调用 | verify(...) |
验证调用次数 | verify(..., times(n)) |
模拟任意参数调用 | any() , anyString() 等 |
1.4.3 示例:UserService测试
在类上一定要使用
@ExtendWith(MockitoExtension.class)
告诉 Mockito“请扫描当前类中的 @Mock、@InjectMocks 注解,创建 Mock 对象并注入”。
class UserServiceTest {
@Mock
private UserMapper userMapper;
@InjectMocks
private UserService userService;
@Test
@DisplayName("测试用户登录成功")
void testLoginSuccess() {
String username = "testUser";
String password = "correctPassword";
String encryptedPassword = PasswordUtil.encrypt(password);
User mockUser = new User(1L, username, encryptedPassword);
when(userMapper.findByUsername(username)).thenReturn(mockUser);
User result = userService.login(username, password);
assertNotNull(result);
assertEquals(username, result.getUsername());
verify(userMapper).findByUsername(username);
}
@Test
@DisplayName("测试用户登录 - 密码错误")
void testLoginWithWrongPassword() {
String username = "testUser";
String correctPassword = "correctPassword";
String wrongPassword = "wrongPassword";
User mockUser = new User(1L, username, PasswordUtil.encrypt(correctPassword));
when(userMapper.findByUsername(username)).thenReturn(mockUser);
assertThrows(AuthenticationException.class, () -> {
userService.login(username, wrongPassword);
});
}
@Test
@DisplayName("测试用户登录 - 用户不存在")
void testLoginWithNonExistentUser() {
String username = "nonExistentUser";
when(userMapper.findByUsername(username)).thenReturn(null);
assertThrows(UserNotFoundException.class, () -> {
userService.login(username, "anyPassword");
});
}
}
二、集成测试
2.1 集成测试 vs 单元测试的区别
维度 | 单元测试(Unit Test) | 集成测试(Integration Test) |
---|---|---|
测试粒度 | 测一个类(如 Service) | 测多个 Bean 的协作(如 Controller→Service→DB) |
是否启动 Spring | ❌ 不启动容器 | ✅ 启动整个 Spring Boot 容器 |
依赖注入方式 | @Mock + @InjectMocks |
@Autowired 注入真实 Bean |
数据库连接 | 无数据库 / Mock Mapper | 真实数据库(H2 或 MySQL) |
测试类上注解 | @ExtendWith(MockitoExtension.class) |
@SpringBootTest |
2.2 Testcontainers 原理与依赖
Testcontainers只是在替代本地运行的MySQL和Redis,并非是真实的生产环境(云端)
Testcontainers 启动的 MySQL/Redis 完全是 Docker 容器里跑的实例,和你机器上手动安装的服务 毫无关系。
Docker 容器里的服务
镜像(例如
mysql:8.0
、redis:7.0
)被拉下来,作为一个隔离的进程组运行在 Docker 守护进程下。容器文件系统、网络端口、存储卷都与本地安装的服务隔离开来。
本地安装的服务
是你通过系统包管理(apt、yum、brew)或官方安装包安装的,运行在系统的服务管理器(systemd、launchctl)中。
因此,使用 Testcontainers 不会影响也不会使用你本地安装的那套 MySQL/Redis;它会新建一个临时、独立、干净的容器环境,测试结束后再把容器删掉。
核心维度 本地安装服务 Testcontainers(推荐测试用) ✅ 测试隔离性 多测试共享服务,易数据污染 每次测试独立容器,自动清理 ✅ 配置一致性 手动配置,版本可能不一致 配置写在测试代码中,团队一致 ✅ 自动化/CI支持 需手动安装服务,CI不友好 自动拉镜像并启动,完美支持 CI 流程 ✅ 启动与销毁 启动慢、需清理 启动快、测试后自动销毁 ✅ 版本切换灵活性 安装/切换麻烦 一行代码换版本(如 mysql:8.0
)
- CI = Continuous Integration(持续集成),指的是每次代码提交都自动触发构建和测试的流程。Testcontainers + CI 能保证在“无人值守”的环境里也能自动启动依赖服务并跑完测试。
“代码即环境”:你在测试代码里声明要用
mysql:8.0
、redis:7.0
镜像,确保每个人本地和 CI 上用的都是同一个版本、同一套配置;Docker 化一致性:Testcontainers 启动的容器和你生产环境里跑的 Docker 容器环境极为相似,能更早地发现容器化部署时才会出现的问题;
零运维负担:不需要在本机或 CI 节点预先安装 MySQL/Redis,只要 Docker 在,就能“一键启动、测试、销毁”。
2.2.1 原理
Testcontainers 是一个 Java 库,内部使用 Docker 启动临时容器,创建真实环境(MySQL、Redis、RabbitMQ等),用于测试。
启动前自动拉取镜像并运行容器;
容器启动后,通过
@DynamicPropertySource
将连接信息注入到 Spring;测试完成后,自动销毁容器,保持测试环境干净。
2.2.2 Testcontainers 不是 Spring Boot 的内置依赖,需要你单独添加。
组件 | 依赖坐标 | 必选 |
---|---|---|
核心库 | org.testcontainers:testcontainers | ✅ |
JUnit | org.testcontainers:junit-jupiter | ✅ |
MySQL | org.testcontainers:mysql | 按需 |
Redis | org.testcontainers:redis | 按需 |
RabbitMQ | org.testcontainers:rabbitmq | 按需 |
Kafka | org.testcontainers:kafka | 按需 |
<!-- 基础测试依赖 -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.16.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.16.3</version>
<scope>test</scope>
</dependency>
<!-- 按需添加模块 -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>1.16.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>redis</artifactId>
<version>1.16.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>rabbitmq</artifactId>
<version>1.16.3</version>
<scope>test</scope>
</dependency>
2.2.3 基础使用方式
@Testcontainers
@SpringBootTest
class MyIntegrationTest {
// 共享容器(所有测试方法共用)
@Container
static final MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
// 动态注入配置
@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
@Test
void testWithRealMySQL() {
// 使用真实MySQL测试...
}
}
2.2.4 复用代码结构方式
// 在src/test/java下创建
public abstract class BaseContainerTest {
@Container
static final MySQLContainer<?> MYSQL = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("test")
.withUsername("test")
.withPassword("test");
@Container
static final RedisContainer REDIS = new RedisContainer("redis:6-alpine");
@DynamicPropertySource
static void setupContainers(DynamicPropertyRegistry registry) {
// 公共配置...
}
}
// 测试类继承即可
class MyTest extends BaseContainerTest {
// 直接使用已启动的容器
}
容器共享:使用
static
容器变量让所有测试方法共享同一容器基类封装:将容器配置放在抽象基类中复用
资源清理:
@AfterEach void cleanup() { // 清理Redis数据 redisTemplate.getConnectionFactory().getConnection().flushAll(); }
2.2.5 容器复用方法
✅ 在全局配置文件中打开复用:
~/.testcontainers.properties(推荐)
testcontainers.reuse.enable=true
或 src/test/resources/testcontainers.properties(也可以)
testcontainers.reuse.enable=true
表示你允许 Testcontainers 启动的容器复用(reuse),即:
不会每次测试都销毁并重新启动 Redis/MySQL/RabbitMQ 容器;
启动速度显著加快(节省 Docker 资源);
尤其适合进行多线程/并发/性能测试。
✅ 开启复用后如何使用:
只要在容器定义时加 .withReuse(true)
:
@Container
static final MySQLContainer<?> MYSQL = new MySQLContainer<>("mysql:8.0")
.withReuse(true); // ✅ 显式声明允许复用
@Container
static final RedisContainer REDIS = new RedisContainer("redis:6-alpine")
.withReuse(true);
✅ 如果不启用复用,会怎样?
每次测试都会启动新容器(30s+延迟),不适合压力测试;
Docker 容器过多还可能残留占用资源;
性能测试时每轮初始化都太慢,无法模拟真实高并发场景。
2.3 集成MySQL
📄 文件结构:
src/
├─ main/
│ └─ resources/
│ └─ application.yml <-- 正式环境配置(MySQL、Redis)
└─ test/
└─ resources/
└─ application-test.yml <-- 测试环境配置(H2、内存配置)
2.3.1 使用 H2 内存数据库测试
✅ 所需依赖(Maven)
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version> <!-- 或使用稳定版 -->
<scope>runtime</scope>
</dependency>
使用 application-test.yml
专用于测试环境
# src/main/resources/application.yml(主配置)
spring:
datasource:
url: jdbc:mysql://localhost:3306/prod_db
driver-class-name: com.mysql.cj.jdbc.Driver
username: prod_user
password: ${DB_PASSWORD}
@SpringBootTest
@Transactional
@Rollback
@ActiveProfiles("test")
class UserMapperH2Test {
@Autowired
private UserMapper userMapper;
@Test
void testInsert() {
User user = new User();
user.setUsername("h2User");
user.setEmail("h2@example.com");
int result = userMapper.insert(user);
assertEquals(1, result);
assertNotNull(user.getId());
}
}
2.3.2 使用Testcontainers MySQL 数据库测试
# src/test/resources/application-test.yml(测试配置)
spring:
datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;MODE=MySQL
driver-class-name: org.h2.Driver
sql:
init:
schema-locations: classpath:schema.sql
data-locations: classpath:data.sql
h2:
console:
enabled: true
path: /h2-console
在集成测试类上统一加上:
@ActiveProfiles("test") // 明确使用测试环境配置 @SpringBootTest
如果不写默认加载
application-test.yml
@SpringBootTest
@Testcontainers
@ActiveProfiles("test")
class UserMapperMySQLTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("test")
.withUsername("test")
.withPassword("test")
.withReuse(true);
@DynamicPropertySource
static void configure(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
registry.add("spring.datasource.driver-class-name", mysql::getDriverClassName);
}
@Autowired
private UserMapper userMapper;
@Test
void testInsert() {
User user = new User();
user.setUsername("tcUser");
user.setEmail("tc@example.com");
int result = userMapper.insert(user);
assertEquals(1, result);
assertNotNull(user.getId());
}
}
@Rollback
注解作用:配合@Transactional
使用,表示测试方法执行完成后回滚事务,不留任何数据痕迹;
2.4 集成Redis
2.4.1 使用 Embedded Redis 测试
Embedded Redis依赖
<dependency>
<groupId>it.ozimov</groupId>
<artifactId>embedded-redis</artifactId>
<version>0.7.3</version>
<scope>test</scope>
</dependency>
特性 | Embedded Redis | 真实Redis/Testcontainers |
---|---|---|
启动速度 | 快(进程内) | 较慢(需启动Docker) |
功能完整性 | 有限(非全功能实现) | 100%兼容 |
调试复杂度 | 简单 | 需要查看容器日志 |
适合场景 | 简单操作验证 | 需要完整Redis特性测试 |
@SpringBootTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class RedisEmbeddedTest {
private RedisServer redisServer;
@BeforeAll
void startRedis() throws IOException {
redisServer = new RedisServer(6379);
redisServer.start();
}
@AfterAll
void stopRedis() {
redisServer.stop();
}
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Test
void testSetGet() {
redisTemplate.opsForValue().set("key", "val");
String value = redisTemplate.opsForValue().get("key");
assertEquals("val", value);
}
}
2.4.2 使用Testcontainers Redis 测试
@SpringBootTest
@Testcontainers
@ActiveProfiles("test")
class RedisTestcontainersTest {
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:7.0")
.withExposedPorts(6379)
.withReuse(true);
@DynamicPropertySource
static void configure(DynamicPropertyRegistry registry) {
registry.add("spring.redis.host", redis::getHost);
registry.add("spring.redis.port", () -> redis.getMappedPort(6379));
}
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Test
void testRedisSetGet() {
redisTemplate.opsForValue().set("k1", "v1");
String value = redisTemplate.opsForValue().get("k1");
assertEquals("v1", value);
}
}
2.5 集成RabbitMQ
2.5.1 混用云端 RabbitMQ 和Testcontainers MySQL/Redis
spring:
rabbitmq:
host: your-cloud-host.aliyuncs.com
port: 5672
username: yourUser
password: yourPass
@Testcontainers
@SpringBootTest
@ActiveProfiles("test")
class MixedIntegrationTest {
// ---- 容器化 MySQL ----
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withReuse(true);
// ---- 容器化 Redis ----
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:7.0")
.withExposedPorts(6379)
.withReuse(true);
@DynamicPropertySource
static void dynamicProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
registry.add("spring.redis.host", redis::getHost);
registry.add("spring.redis.port", () -> redis.getMappedPort(6379));
// 注意:rabbitmq 不在这里重写,还是用 application-test.yml 里的云端配置
}
@Autowired
private RabbitTemplate rabbitTemplate; // 会连接到阿里云的 RabbitMQ
@Test
void testCloudRabbitAndLocalDb() {
// 1)数据库操作走 Testcontainers 提供的 MySQL 容器
// 2)缓存操作走 Testcontainers 提供的 Redis 容器
// 3)消息操作走阿里云 RabbitMQ
Object msg = rabbitTemplate.receiveAndConvert("cloud.test.queue");
// ...
}
}
Testcontainers 只管理那些你用
@Container
启动的服务。对于没有容器化声明的组件(如上例的 RabbitMQ),Spring 还是会按配置文件(
application-test.yml
或application.yml
)去连接阿里云。这样就可以既用本地 Docker 容器来做数据库/缓存的隔离测试,又用云端实例来做消息队列的功能联调。
2.5.2 使用 Testcontainers 启动 RabbitMQ 容器
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.testcontainers.containers.RabbitMQContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Testcontainers
@SpringBootTest
@ActiveProfiles("test")
class RabbitMQTestContainersTest {
@Container
static final RabbitMQContainer rabbitMQ =
new RabbitMQContainer(DockerImageName.parse("rabbitmq:3.10-management"))
.withExposedPorts(5672, 15672)
.withReuse(true);
@DynamicPropertySource
static void rabbitProperties(DynamicPropertyRegistry registry) {
registry.add("spring.rabbitmq.host", rabbitMQ::getHost);
registry.add("spring.rabbitmq.port", rabbitMQ::getAmqpPort);
registry.add("spring.rabbitmq.username", rabbitMQ::getAdminUsername);
registry.add("spring.rabbitmq.password", rabbitMQ::getAdminPassword);
}
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void testSendAndReceive() {
String queue = "tc.test.queue";
// 在容器中声明队列
rabbitTemplate.execute(channel -> {
channel.queueDeclare(queue, false, false, false, null);
return null;
});
// 发送并接收消息
rabbitTemplate.convertAndSend(queue, "hello-tc");
Object received = rabbitTemplate.receiveAndConvert(queue);
assertEquals("hello-tc", received);
}
}