我们的大多数应用程序都必须与数据库,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 = new GenericContainer("docker.elastic.co/elasticsearch/elasticsearch:6.1.1") |
我们可以看到使用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=true |
最后,我们可以使用stop方法停止我们的容器。
container.stop(); |
这将停止容器并移除附加的卷。这很棒,因为它可以防止悬空卷。
在测试期间
Testcontainers的一大优势在于它与JUnit框架的集成。实际上,GenericContainer对象是JUnit规则。这意味着它们的生命周期直接与测试生命周期绑定。因此,通过使用@Rule或@ClassRuleJUnit注释,我们的容器将在测试启动之前初始化,并在测试执行结束时停止。
@ClassRule |
尽管如此,这意味着Testcontainers将带有JUnit 4依赖项,如果您的测试使用JUnit 5运行,则会很烦人。实际上,JUnit已经用Extension扩展了Rule规则。从2018年11月发布的1.10.0版本开始,Testcontainers 现在支持JUnit 5,并且可以在专用库junit-jupiter 的帮助@Testcontainers和@Container注释中使用扩展:
<dependency> |
预配置的容器
像Docker一样,Testcontainers生态系统非常丰富。您可以找到预配置的容器,如MySQL,PostgreSQL,Oracle数据库,Kafka,Neo4j,Elasticsearch等。
@Rule |
您可以直接从maven存储库浏览列表。
具体案例
让我们看一下使用Spring PetClinic应用程序使用Testscontainers的具体示例。这是一个基于Spring Boot,Spring MVC和Spring JPA等几个Spring组件的演示项目。该应用程序旨在管理宠物诊所与宠物,宠物主人和兽医。
控制器层公开HTTP端点以创建和读取实体。然后,持久层与关系数据库通信。可以将应用程序配置为与HSQLDB或MySQL数据库通信。
持久层使用集成测试进行测试,它们使用内存中的HSQL数据库,而持久层本身使用MySQL数据库。
要求
首先,我们必须在要执行测试的机器上安装Docker。然后,我们需要将Testcontainers依赖项添加到项目中。在这种情况下,我们只需将以下内容添加到pom.xml文件中:
<dependency> |
数据库配置
默认数据库配置在application.properties文件中完成。
database=hsqldb |
我们可以看到,这是一个使用schema.sql文件中的模式初始化的内存中HSQLDB数据库。然后,使用data.sql文件填充数据库。这是默认的项目配置。
我们需要创建application-test.properties文件来配置与MySQL数据库的连接。
spring.datasource.url=jdbc:mysql://localhost/petclinic |
接下来,让我们参加测试类ClinicServiceTests.java。此类包含持久层的所有集成测试。首先,我们需要更改Spring测试配置以确保测试将使用我们的数据库连接。
@RunWith(SpringRunner.class) |
TestPropertySource注释能够载入我们的文件application-test.properties和AutoConfigureTestDatabase与NONE值可防止Spring创建一个嵌入式数据库。
MySQL容器
让我们创建一个匹配测试要求的MySQL数据库。在这种情况下,我们使用Testcontainers的功能从动态创建的Dockerfile创建Docker镜像。作为第一步,我们从Docker Hub中提取了MySQL官方图像:
@ClassRule |
现在,我们必须创建我们的数据库和连接的用户。这是通过使用Docker镜像中的环境变量来完成的。
@ClassRule |
接下来,我们必须创建一个数据库模式并填充数据库。镜像文件中的目录/docker-entrypoint-initdb.d在启动时被扫描,所有带有 .sh ,.sql 或 .sql.gz扩展名的文件都被执行 。所以,我们只要把我们的文件schema.sql文件和data.sql此目录中。
@ClassRule |
通过使用withClasspathResourceMapping,文件schema.sql文件和data.sql被放置在类路径从而进入容器作为它的一个卷。然后,我们可以在我们的Dockerfile构造中访问它。
最后一件事,我们必须公开默认的MySQL端口:3306。
@ClassRule |
不幸的是,我们无法直接使用该方法设置端口绑定setPortBindings。我们必须在创建时使用withCreateContainerCmdModifier方法自定义容器。最后,我们正在等待监听端口以确保我们的容器已启动。
瞧!只需几行代码,我们就可以轻松地为我们的测试设置MySQL数据库,而无需管理容器生命周期。该@ClassRule注释使我们的容器,所有的测试启动一次。您可能想知道:我们是否延长了测试执行时间?实际上,使用HSQLDB内存数据库时,Docker容器只需要907毫秒,而860毫秒。本节中显示的源代码可在github上找到。