【本文内容】
- 介绍
Testcontainers
- 【示例】
Spring Boot
+JUnit5
+MySQL
,用Testcontainers启动容器测试 - 【示例】
Spring Boot
+JUnit5
+Redis
,用Testcontainers启动容器测试 - 其它功能:
- 与JUnit5集成的注解:
@Testcontainers
,@Container
- 镜像pull的policy
- re-use容器(超详细)
- 与JUnit5集成的注解:
1. 什么是Testcontainers
官网:https://www.testcontainers.org/
Testcontainers是一个Java第三方类库,如同它的名字,test+container,即以容器的方式进行测试。目前支持的测试框架有:JUnit4 / JUnit5 / Spock。
最常见的案例有:项目中有数据库甚至缓存的依赖,那么传统测试就必须连到一个真实的数据库或缓存服务器,这个数据库/缓存可以是远程服务器的,也可以是本地自己安装的,甚至是本地的Docker容器。
那么Testcontainers做的就是在测试启动的时候,帮助我们pull docker image并启动,这样我们就可以直接跑单元测试了,而不需要有真实的数据库或缓存服务器。
从官网的Module中也可以看到,Testcontainers支持大部分的数据库产品,以及其它中间件如Ngnx,cache相关,消息中间件如Kafka / RabbitMQ等等: image.png image.png2. 示例:Spring Boot + JUnit5 + MySQL,用Testcontainers启动容器测试
在使用Testcontainers之前,需要先在本地安装docker环境。
【参考】
以下两个示例都是Spring Boot + postgresql,用Testcontainers启动容器测试:
示例1:
- https://www.youtube.com/watch?v=-mYJKwrySOw
- https://github.com/rieckpil/blog-tutorials/tree/master/testcontainers-youtube-series(即上述视频中演示的源代码)
示例2:
2.1 依赖
首先是Spring Boot相关的依赖:
- parant,我用的是2.5.7版本
- web, jpa都是比较常见的starter。前者引入了mvc相关的,后者是操作数据库相关的。
- Spring boot test starter也是常见的依赖。
testcontainers相关:
-
org.testcontainers.mysql
,每个Testcontainer模块都有自己的依赖,比如数据库实现是postgresql,那么就改成postgresql。它会额外引入testcontainers这个包。 -
org.testcontainers.junit-jupiter
,这个会引入和JUnit集成的相关类,如org.testcontainers.junit.jupiter.Testcontainers
。
另外为了自动创建表结构,引入了flyway
,参考:https://blog.csdn.net/qianzhitu/article/details/110629847
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.7</version>
</parent>
<artifactId>TestcontainersWithSpringBoot</artifactId>
<dependencies>
< !-- Spring Boot相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
< !-- Spring JPA 相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
< !-- mysql 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.18</version>
</dependency>
< !-- 帮助我们创建表结构 -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
< !-- testcontainers 依赖 -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>1.17.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.17.3</version>
<scope>test</scope>
</dependency>
< !-- Spring Boot 测试,2.4.0(+)默认引用的就是JUnit5 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.18</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
2.2 创建一个entity
只有两列:id和name:
@Data
@Entity
@Table(name = "course")
public class Course {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Column(name = "name")
private String name;
}
2.3 flywaydb相关
在resources/db/migration下创建sql文件:V001__INIT.sql
:
CREATE TABLE `course` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(45) NULL,
PRIMARY KEY (`id`));
2.4 其它的常规类省略:
- CourseRepository接口。
- SpringBoot入口类。
- application.yaml包含spring.datasource.url等mysql数据库配置。
2.5 创建Test类
最后,当然也是最最重要的,创建Test类:
- 首先需要加上注解:
@Testcontainers
- 创建一个MySQLContainer对象,可以自定义镜像tag,我定义的是5.7版本,如果不定义,默认下载的是latest版本,即最新版。
- 通过
@DynamicPropertySource
注解来重写Properties,传入的是当前docker run的mysql容器,而不是application.yaml中定义的数据库。
@Testcontainers
@SpringBootTest
public class CourseRepositoryTest {
@Autowired
private CourseRepository courseRepository;
@Container
private static MySQLContainer mySQLContainer = new MySQLContainer("mysql:5.7")
.withDatabaseName("test")
.withUsername("root")
.withPassword("root");
@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mySQLContainer::getJdbcUrl);
registry.add("spring.datasource.password", mySQLContainer::getPassword);
registry.add("spring.datasource.username", mySQLContainer::getUsername);
}
@Test
public void saveAndGetTest() {
Course course = new Course();
course.setName("test course");
courseRepository.save(course);
Course newCourse = courseRepository.findById(1).get();
Assertions.assertEquals(1, newCourse.getId());
Assertions.assertEquals("test course", newCourse.getName());
}
}
3. 与Redis集成
首先除了Spring Boot相关的依赖以及Testcontainers相关的依赖外,需要引入redis相关的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
创建测试类,我使用的是redis:6.0.0的镜像,需要在启动后把host和port传回给系统变量spring.redis.host
以及spring.redis.port
,这样可以让String通过AutoConfiguration的方式创建RedisTemplate:
@Testcontainers
@SpringBootTest
public class RedisTest {
MySQLContainer<?> container = CustomMySQLContainer.getInstance();
static {
GenericContainer redisContainer = new GenericContainer<>(DockerImageName.parse("redis:6.0.0")).withExposedPorts(6379);
redisContainer.start();
System.setProperty("spring.redis.host", redisContainer.getHost());
System.setProperty("spring.redis.port", redisContainer.getMappedPort(6379).toString());
}
@Autowired
private RedisTemplate redisTemplate;
@Test
public void test() {
ValueOperations<String, String> stringRedis = redisTemplate.opsForValue();
stringRedis.set("name", "valuetest");
System.out.println(stringRedis.get("name"));
}
}
4. 其它功能
4.1 JUnit 5集成
https://www.testcontainers.org/quickstart/junit_5_quickstart/
上述#2.5中有两个注解,这两个注解都是org.testcontainers.junit-jupiter包中,即JUnit5集成的包:
@Testcontainers
@Container
@Container注解会管理container的生命周期:
- 在@Test运行前就先开始pull docker镜像,如果本地docker有镜像,那么根据默认的policy,会优先使用本地镜像。
- docker运行一个容器
- 在test运行后关闭一个容器。
使用@Container
会自动启动该容器,也可手动管理容器,即使用容器start()
方法,即:
static {
mySQLContainer = new MySQLContainer<>("mysql:5.7")
.withDatabaseName("test")
.withUsername("root1")
.withPassword("root1");
mySQLContainer.start();
}
4.2 镜像pull的policy
官网:https://www.testcontainers.org/features/advanced_options/#image-pull-policy
默认情况下:
- 如果有设置具体的镜像版本:容器的image是从本地的Docker image缓存中拿,如果本地docker中没有,才去docker hub中心拉取镜像。
- 如果设置的镜像版本为latest,那么如果本地镜像中有该镜像,也会直接使用,但可能会导致不会使用docker hub真正的最新的版本了。
如果想要每次都从docker hub中拉取:
GenericContainer<?> container = new GenericContainer<>(imageName)
.withImagePullPolicy(PullPolicy.alwaysPull())
也可自己实现一个ImagePullPolicy,详细请参考官网。
4.3 re-use容器
参考:
- https://stackoverflow.com/questions/62425598/how-to-reuse-testcontainers-between-multiple-springboottests
- https://rieckpil.de/reuse-containers-with-testcontainers-for-fast-integration-tests/
每个@SpringBootTest类,都需要使用Testcontainer容器测试,如果使用注解@Container
,那么如同#4.1中说的,这个注解每次都会stop该容器。
如果我们有两个Test类,如何实现共用同一个容器呢?即Testcontainer的re-use。
根据上述网站上的介绍,想要达到这一目的,有两种方式:
- 【方式一】可以使用
GenericContainer
类的withReuse(true)
+ 本地~/.testcontainers.properties
中声明testcontainers.reuse.enable=true
的方式来重用容器。(注,必须是两者都要set,只在代码中声明withReuse(true)是不够的)。 - 【方式二】可以使用人工管理Testcontainer的生命周期,即手动调用
start()
方法,再将Testcontainer变成单例模式。
方式一的示例代码:withReuse(true)
+ ~/.testcontainers.properties
:
首先需要在用户个人目录中的.testcontainers.properties
文件添加一行testcontainers.reuse.enable=true
:
再创建测试类:
Test类1:
@SpringBootTest
public class CourseRepositoryFirstTest {
@Autowired
private CourseRepository courseRepository;
public static MySQLContainer<?> mySQLContainer = new MySQLContainer<>("mysql:5.7")
.withDatabaseName("test")
.withUsername("root")
.withPassword("root")
.withReuse(true);
@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mySQLContainer::getJdbcUrl);
registry.add("spring.datasource.password", mySQLContainer::getPassword);
registry.add("spring.datasource.username", mySQLContainer::getUsername);
}
@BeforeAll
public static void start() {
mySQLContainer.start();
}
@Test
public void saveAndGetTest() {
Course course = new Course();
course.setName("test course first");
courseRepository.save(course);
System.out.println(courseRepository.findAll());
}
}
Test类2:
@SpringBootTest
public class CourseRepositorySecondTest {
@Autowired
private CourseRepository courseRepository;
public static MySQLContainer<?> mySQLContainer = new MySQLContainer<>("mysql:5.7")
.withDatabaseName("test")
.withUsername("root")
.withPassword("root")
.withReuse(true);
@DynamicPropertySource
static void properties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mySQLContainer::getJdbcUrl);
registry.add("spring.datasource.password", mySQLContainer::getPassword);
registry.add("spring.datasource.username", mySQLContainer::getUsername);
}
@BeforeAll
public static void start() {
mySQLContainer.start();
}
@Test
public void saveAndGetTest() {
Course course = new Course();
course.setName("test course second");
courseRepository.save(course);
System.out.println(courseRepository.findAll());
}
}
测试1的打印结果:[Course(id=1, name=test course second)]
测试2的打印结果:[Course(id=1, name=test course second), Course(id=2, name=test course first)]
测试2中也能打印出测试1中插入的数据,说明他们共享了同一个container容器。
方式2的示例代码:使用单例模式,手动启动容器的方式:
首先是新建一个单例模式的MySQLContainer:
public class CustomMySQLContainer extends MySQLContainer<CustomMySQLContainer> {
private static CustomMySQLContainer mySQLContainer;
public CustomMySQLContainer() {
super("mysql:5.7");
self().withDatabaseName("test").withUsername("root").withPassword("root");
}
public static MySQLContainer getInstance() {
if (mySQLContainer == null) {
mySQLContainer = new CustomMySQLContainer();
mySQLContainer.start();
System.setProperty("DB_URL", mySQLContainer.getJdbcUrl());
System.setProperty("DB_USERNAME", mySQLContainer.getUsername());
System.setProperty("DB_PASSWORD", mySQLContainer.getPassword());
}
return mySQLContainer;
}
}
为了可以让System.setProperty顺利的set进去,修改application.yaml:
spring:
datasource:
url: ${DB_URL:jdbc:mysql://localhost:3306/flyway_test?useUnicode=true&characterEncoding=UTF-8}
username: ${DB_USERNAME:root}
password: ${DB_PASSWORD:123456}
driver-class-name: com.mysql.jdbc.Driver
然后是两个测试用例:
测试类1:
@SpringBootTest
public class CourseRepositoryFirstTest {
@Autowired
private CourseRepository courseRepository;
MySQLContainer<?> container = CustomMySQLContainer.getInstance();
@Test
public void saveAndGetTest() {
Course course = new Course();
course.setName("test course first");
courseRepository.save(course);
System.out.println(courseRepository.findAll());
}
}
测试类2:
@SpringBootTest
public class CourseRepositorySecondTest {
@Autowired
private CourseRepository courseRepository;
MySQLContainer<?> container = CustomMySQLContainer.getInstance();
@Test
public void saveAndGetTest() {
Course course = new Course();
course.setName("test course second");
courseRepository.save(course);
System.out.println(courseRepository.findAll());
}
}
测试类1打印结果:[Course(id=1, name=test course second)]
测试类2打印结果:[Course(id=1, name=test course second), Course(id=2, name=test course first)]
通过单例模式+手动开启start(),也能实现容器的复用。
4.4 需要预先安装docker环境
在第2章一开始就有讲过,Testcontainers会在本地的docker中运行容器,所以需要本地的环境预先装有docker。
在docker命令行输入docker ps -a
,也可以看到我们启动过的docker镜像:
如果启动的时候有点慢,可以先用docker pull命令先把远程的镜像下载到本地后再运行测试用例。
网友评论