我们的大多数应用程序都必须与数据库,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代码响应来确保我们的容器已启动。
还有其他策略断言容器正在运行:
要完成容器配置,我们的容器将公开内部端口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上找到 。