Java后端测试
一、单元测试
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;@BeforeAllstatic void initDatabase() {connection = Database.connect(); // 所有测试共享的昂贵资源}@AfterAllstatic void closeDatabase() {connection.close(); // 所有测试完成后清理资源}@Test void testQuery1() { /* 使用connection */ }@Test void testQuery2() { /* 使用connection */ }
}
(2)@BeforeEach
和 @AfterEach
执行时机:
@BeforeEach
:在每个测试方法执行之前运行@AfterEach
:在每个测试方法执行之后运行
非静态方法:不需要static
修饰
典型用途:
class CalculatorTest {Calculator calculator;@BeforeEachvoid init() {calculator = new Calculator(); // 每个测试前创建新实例}@AfterEachvoid 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 {@Mockprivate UserMapper userMapper;@InjectMocksprivate UserService userService;@Testvoid 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 {@Mockprivate UserMapper userMapper;@InjectMocksprivate 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 {// 共享容器(所有测试方法共用)@Containerstatic final MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");// 动态注入配置@DynamicPropertySourcestatic 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);}@Testvoid testWithRealMySQL() {// 使用真实MySQL测试...}
}
2.2.4 复用代码结构方式
// 在src/test/java下创建
public abstract class BaseContainerTest {@Containerstatic final MySQLContainer<?> MYSQL = new MySQLContainer<>("mysql:8.0").withDatabaseName("test").withUsername("test").withPassword("test");@Containerstatic final RedisContainer REDIS = new RedisContainer("redis:6-alpine");@DynamicPropertySourcestatic 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_dbdriver-class-name: com.mysql.cj.jdbc.Driverusername: prod_userpassword: ${DB_PASSWORD}
@SpringBootTest
@Transactional
@Rollback
@ActiveProfiles("test")
class UserMapperH2Test {@Autowiredprivate UserMapper userMapper;@Testvoid 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=MySQLdriver-class-name: org.h2.Driversql:init:schema-locations: classpath:schema.sqldata-locations: classpath:data.sqlh2:console:enabled: truepath: /h2-console
在集成测试类上统一加上:
@ActiveProfiles("test") // 明确使用测试环境配置 @SpringBootTest
如果不写默认加载
application-test.yml
@SpringBootTest
@Testcontainers
@ActiveProfiles("test")
class UserMapperMySQLTest {@Containerstatic MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0").withDatabaseName("test").withUsername("test").withPassword("test").withReuse(true);@DynamicPropertySourcestatic 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);}@Autowiredprivate UserMapper userMapper;@Testvoid 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;@BeforeAllvoid startRedis() throws IOException {redisServer = new RedisServer(6379);redisServer.start();}@AfterAllvoid stopRedis() {redisServer.stop();}@Autowiredprivate RedisTemplate<String, String> redisTemplate;@Testvoid 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 {@Containerstatic GenericContainer<?> redis = new GenericContainer<>("redis:7.0").withExposedPorts(6379).withReuse(true);@DynamicPropertySourcestatic void configure(DynamicPropertyRegistry registry) {registry.add("spring.redis.host", redis::getHost);registry.add("spring.redis.port", () -> redis.getMappedPort(6379));}@Autowiredprivate RedisTemplate<String, String> redisTemplate;@Testvoid 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.comport: 5672username: yourUserpassword: yourPass
@Testcontainers
@SpringBootTest
@ActiveProfiles("test")
class MixedIntegrationTest {// ---- 容器化 MySQL ----@Containerstatic MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0").withReuse(true);// ---- 容器化 Redis ----@Containerstatic GenericContainer<?> redis = new GenericContainer<>("redis:7.0").withExposedPorts(6379).withReuse(true);@DynamicPropertySourcestatic 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 里的云端配置}@Autowiredprivate RabbitTemplate rabbitTemplate; // 会连接到阿里云的 RabbitMQ@Testvoid testCloudRabbitAndLocalDb() {// 1)数据库操作走 Testcontainers 提供的 MySQL 容器// 2)缓存操作走 Testcontainers 提供的 Redis 容器// 3)消息操作走阿里云 RabbitMQObject 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 {@Containerstatic final RabbitMQContainer rabbitMQ =new RabbitMQContainer(DockerImageName.parse("rabbitmq:3.10-management")).withExposedPorts(5672, 15672).withReuse(true);@DynamicPropertySourcestatic 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);}@Autowiredprivate RabbitTemplate rabbitTemplate;@Testvoid 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);}
}