美文网首页
【测试相关】Testcontainers介绍,与Spring B

【测试相关】Testcontainers介绍,与Spring B

作者: 伊丽莎白2015 | 来源:发表于2022-07-17 20:26 被阅读0次

【本文内容】

  • 介绍Testcontainers
  • 【示例】Spring Boot + JUnit5 + MySQL,用Testcontainers启动容器测试
  • 【示例】Spring Boot + JUnit5 + Redis,用Testcontainers启动容器测试
  • 其它功能:
    • 与JUnit5集成的注解:@Testcontainers@Container
    • 镜像pull的policy
    • re-use容器(超详细)

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.png

2. 示例:Spring Boot + JUnit5 + MySQL,用Testcontainers启动容器测试

在使用Testcontainers之前,需要先在本地安装docker环境。

【参考】
以下两个示例都是Spring Boot + postgresql,用Testcontainers启动容器测试:

示例1:

示例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数据库配置。
项目目录: image.png
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容器

参考:

每个@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

image.png image.png

再创建测试类:
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的示例代码:使用单例模式,手动启动容器的方式:

官网:https://www.testcontainers.org/test_framework_integration/manual_lifecycle_control/#singleton-containers

首先是新建一个单例模式的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镜像:

image.png

如果启动的时候有点慢,可以先用docker pull命令先把远程的镜像下载到本地后再运行测试用例。

相关文章

网友评论

      本文标题:【测试相关】Testcontainers介绍,与Spring B

      本文链接:https://www.haomeiwen.com/subject/unaabrtx.html