转载

用Testcontainers实现SpringBoot+Docker集成测试

我们的大多数应用程序都必须与数据库,HTTP API,消息代理,SMTP服务器等进行通信......使用这些组件设置真正的测试环境非常复杂。

在某些情况下,我们可以在测试执行期间简单地模拟这些组件或具有内存中的组件。例如,H2 或HSQLDB是众所周知的在集成测试期间使用的内存数据库。但是,它们不是生产环境中使用的,我们的测试似乎没有代表性。

今天, 借助Testcontainers, 可以轻松利用Docker的所有功能并轻松建立连接的测试环境。

Testcontainers

Testcontainers允许我们在测试执行期间轻松操作Docker容器。它使用Docker客户端 docker-java 与Docker守护进程通信。它适用于大多数操作系统和环境,尽管对Windows提供了最大的支持,但我每天都使用Docker Toolbox。您可以在 此处 看看与你的操作系统兼容性。

当你创建一个容器,Testcontainers将尝试使用连接到Docker 守护进程,这是通过DOCKER_HOST,DOCKER_TLS_VERIFY和DOCKER_CERT_PATH环境变量实现,可以在JVM中轻松覆盖这些环境变量。

创建一个容器

容器是使用对象GenericContainer表示。可以从镜像、Dockerfile或动态创建的Dockerfile创建容器。此外,还可以从 Docker Compose文件 创建容器。

例如,这是一个从镜像 docker.elastic.co/elasticsearch/elasticsearch:6.1.1 创建的Elasticsearch服务器。

GenericContainer container = <b>new</b> GenericContainer(<font>"docker.elastic.co/elasticsearch/elasticsearch:6.1.1"</font><font>)
 .withEnv(</font><font>"discovery.type"</font><font>, </font><font>"single-node"</font><font>)
 .withExposedPorts(9200)
 .waitingFor(
   Wait
   .forHttp(</font><font>"/_cat/health?v&pretty"</font><font>)
   .forStatusCode(200)
 );
</font>

我们可以看到使用withEnv方法向容器提供环境变量相当容易。在这个案例中,设置了变量discovery.type是单节点。

接下来,我们通过对/_cat/healthAPI 进行HTTP调用并具有200代码响应来确保我们的容器已启动。

还有其他策略断言容器正在运行:

  • Wait.forLogMessage等待日志消息,
  • Wait.forListeningPort等待侦听端口
  • Wait.forHealthcheck允许使用 docker中的HEALTHCHECK 功能。

要完成容器配置,我们的容器将公开内部端口9200,并使用该方法显式设置withExposedPorts。这意味着Testcontainers会将此容器的端口映射到随机端口。可以使用该方法检索映射端口,getMappedPort否则我们可以使用该方法定义端口绑定setPortBindings。在这里,我们将端口9200从容器暴露到端口9200:

container.setPortBindings(Arrays.asList(“9200:9200”));

我们的Elasticsearch服务器已准备好使用。要启动它,我们只需要执行start方法:

container.start();

在启动时,Testcontainers将运行一系列检查,如docker版本或与已注册Docker Registry的连接。如果您在公司代理后面工作,这可能会阻塞,因此可以通过使用以下内容在tests资源目录中创建文件testcontainers.properties来禁用这些检查:

check.disable=<b>true</b>

最后,我们可以使用stop方法停止我们的容器。

container.stop();

这将停止容器并移除附加的卷。这很棒,因为它可以防止悬空卷。

在测试期间

Testcontainers的一大优势在于它与JUnit框架的集成。实际上,GenericContainer对象是 JUnit规则 。这意味着它们的生命周期直接与测试生命周期绑定。因此,通过使用@Rule或@ClassRuleJUnit注释,我们的容器将在测试启动之前初始化,并在测试执行结束时停止。

@ClassRule
<b>public</b> <b>static</b> GenericContainer redis = <b>new</b> GenericContainer(<font>"redis:3.0.2"</font><font>)
 .withExposedPorts(6379);
</font>

尽管如此,这意味着Testcontainers将带有JUnit 4依赖项,如果您的测试使用 JUnit 5 运行,则会很烦人。实际上,JUnit已经用 Extension扩展 了Rule规则。从2018年11月发布的1.10.0版本开始,Testcontainers  现在支持JUnit 5 ,并且可以在专用库junit-jupiter 的帮助@Testcontainers和@Container注释中使用扩展:

<dependency>
 <groupId>testcontainers</groupId>
 <artifactId>junit-jupiter</artifactId>
 <version>1.10.2</version>
</dependency>

预配置的容器

像Docker一样,Testcontainers生态系统非常丰富。您可以找到预配置的容器,如MySQL,PostgreSQL,Oracle数据库,Kafka,Neo4j,Elasticsearch等。

@Rule
<b>public</b> KafkaContainer kafka = <b>new</b> KafkaContainer();

您可以直接从 maven存储库 浏览列表。

具体案例

让我们看一下使用 Spring PetClinic应用程序 使用Testscontainers的具体示例。这是一个基于Spring Boot,Spring MVC和Spring JPA等几个Spring组件的演示项目。该应用程序旨在管理宠物诊所与宠物,宠物主人和兽医。

控制器层公开HTTP端点以创建和读取实体。然后,持久层与关系数据库通信。可以将应用程序配置为与HSQLDB或MySQL数据库通信。

持久层使用集成测试进行测试,它们使用内存中的HSQL数据库,而持久层本身使用MySQL数据库。

要求

首先,我们必须在要执行测试的机器上安装Docker。然后,我们需要将Testcontainers依赖项添加到项目中。在这种情况下,我们只需将以下内容添加到pom.xml文件中:

<dependency>
  <groupId>org.testcontainers</groupId>
  <artifactId>testcontainers</artifactId>
  <version>1.10.2</version>
  <scope>test</scope>
</dependency>

数据库配置

默认数据库配置在application.properties文件中完成。

database=hsqldb
spring.datasource.schema=classpath*:db/${database}/schema.sql
spring.datasource.data=classpath*:db/${database}/data.sql

我们可以看到,这是一个使用schema.sql文件中的模式初始化的内存中HSQLDB数据库。然后,使用data.sql文件填充数据库。这是默认的项目配置。

我们需要创建application-test.properties文件来配置与MySQL数据库的连接。

spring.datasource.url=jdbc:mysql:<font><i>//localhost/petclinic</i></font><font>
spring.datasource.username=petclinic
spring.datasource.password=petclinic
spring.datasource.driver-<b>class</b>-name=com.mysql.jdbc.Driver
</font>

接下来,让我们参加测试类ClinicServiceTests.java。此类包含持久层的所有集成测试。首先,我们需要更改Spring测试配置以确保测试将使用我们的数据库连接。

@RunWith(SpringRunner.<b>class</b>)
@DataJpaTest(includeFilters = @ComponentScan.Filter(Service.<b>class</b>))
@TestPropertySource(locations=<font>"classpath:application-test.properties"</font><font>)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
<b>public</b> <b>class</b> ClinicServiceTests {
...
}
</font>

TestPropertySource注释能够载入我们的文件application-test.properties和AutoConfigureTestDatabase与NONE值可防止Spring创建一个嵌入式数据库。

MySQL容器

让我们创建一个匹配测试要求的MySQL数据库。在这种情况下,我们使用Testcontainers的功能从动态创建的Dockerfile创建Docker镜像。作为第一步,我们从Docker Hub中提取了 MySQL官方图像 :

@ClassRule
<b>public</b> <b>static</b> GenericContainer mysql = <b>new</b> GenericContainer(
  <b>new</b> ImageFromDockerfile(<font>"mysql-petclinic"</font><font>)
    .withDockerfileFromBuilder(dockerfileBuilder -> {
       dockerfileBuilder.from(</font><font>"mysql:5.7.8"</font><font>)
    }
);
</font>

现在,我们必须创建我们的数据库和连接的用户。这是通过使用Docker镜像中的环境变量来完成的。

@ClassRule
<b>public</b> <b>static</b> GenericContainer mysql = <b>new</b> GenericContainer(
  <b>new</b> ImageFromDockerfile(<font>"mysql-petclinic"</font><font>)
   .withDockerfileFromBuilder(dockerfileBuilder -> {
      dockerfileBuilder.from(</font><font>"mysql:5.7.8"</font><font>)
      </font><font><i>// root password is mandatory</i></font><font>
      .env(</font><font>"MYSQL_ROOT_PASSWORD"</font><font>, </font><font>"root_password"</font><font>)
      .env(</font><font>"MYSQL_DATABASE"</font><font>, </font><font>"petclinic"</font><font>)
      .env(</font><font>"MYSQL_USER"</font><font>, </font><font>"petclinic"</font><font>)
      .env(</font><font>"MYSQL_PASSWORD"</font><font>, </font><font>"petclinic"</font><font>)
 })
</font>

接下来,我们必须创建一个数据库模式并填充数据库。镜像文件中的目录/docker-entrypoint-initdb.d在启动时被扫描,所有带有 .sh ,.sql 或  .sql.gz扩展名的文件都被执行  。所以,我们只要把我们的文件schema.sql文件和data.sql此目录中。

@ClassRule
<b>public</b> <b>static</b> GenericContainer mysql = <b>new</b> GenericContainer(
  <b>new</b> ImageFromDockerfile(<font>"mysql-petclinic"</font><font>)
    .withDockerfileFromBuilder(dockerfileBuilder -> {
      dockerfileBuilder.from(</font><font>"mysql:5.7.8"</font><font>)
     .env(</font><font>"MYSQL_ROOT_PASSWORD"</font><font>, </font><font>"root_password"</font><font>)
     .env(</font><font>"MYSQL_DATABASE"</font><font>, </font><font>"petclinic"</font><font>)
     .env(</font><font>"MYSQL_USER"</font><font>, </font><font>"petclinic"</font><font>)
     .env(</font><font>"MYSQL_PASSWORD"</font><font>, </font><font>"petclinic"</font><font>)
     .add(</font><font>"a_schema.sql"</font><font>, </font><font>"/docker-entrypoint-initdb.d"</font><font>)
     .add(</font><font>"b_data.sql"</font><font>, </font><font>"/docker-entrypoint-initdb.d"</font><font>);
 })
 .withFileFromClasspath(</font><font>"a_schema.sql"</font><font>, </font><font>"db/mysql/schema.sql"</font><font>)
 .withFileFromClasspath(</font><font>"b_data.sql"</font><font>, </font><font>"db/mysql/data.sql"</font><font>))
</font>

通过使用 withClasspathResourceMapping ,文件schema.sql文件和data.sql被放置在类路径从而进入容器作为它的一个卷。然后,我们可以在我们的Dockerfile构造中访问它。

最后一件事,我们必须公开默认的MySQL端口:3306。

@ClassRule
<b>public</b> <b>static</b> GenericContainer mysql = <b>new</b> GenericContainer(
  <b>new</b> ImageFromDockerfile(<font>"mysql-petclinic"</font><font>)
    .withDockerfileFromBuilder(dockerfileBuilder -> {
      ....
    })
  .withExposedPorts(3306)
  .withCreateContainerCmdModifier(
    <b>new</b> Consumer<CreateContainerCmd>() {   
     @Override
     <b>public</b> <b>void</b> accept(CreateContainerCmd createContainerCmd) {
       createContainerCmd.withPortBindings(
         <b>new</b> PortBinding(Ports.Binding.bindPort(3306), <b>new</b> ExposedPort(3306))
       );
    }
 })
 .waitingFor(Wait.forListeningPort());
</font>

不幸的是,我们无法直接使用该方法设置端口绑定setPortBindings。我们必须在创建时使用withCreateContainerCmdModifier方法 自定义容器 。最后,我们正在等待监听端口以确保我们的容器已启动。

瞧!只需几行代码,我们就可以轻松地为我们的测试设置MySQL数据库,而无需管理容器生命周期。该@ClassRule注释使我们的容器,所有的测试启动一次。您可能想知道:我们是否延长了测试执行时间?实际上,使用HSQLDB内存数据库时,Docker容器只需要907毫秒,而860毫秒。本节中显示的源代码可在 github上找到 。

原文  https://www.jdon.com/51327
正文到此结束
Loading...