美文网首页
【测试相关】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