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

2022-07-17  本文已影响0人  伊丽莎白2015

【本文内容】


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相关的依赖:

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 其它的常规类省略:
项目目录: image.png
2.5 创建Test类

最后,当然也是最最重要的,创建Test类:

@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集成的包:

@Container注解会管理container的生命周期:

使用@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

默认情况下:

如果想要每次都从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。

根据上述网站上的介绍,想要达到这一目的,有两种方式:

方式一的示例代码: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命令先把远程的镜像下载到本地后再运行测试用例。

上一篇下一篇

猜你喜欢

热点阅读