本文是我们学院课程中名为《 面向Java开发人员的Docker教程 》的一部分。
在本课程中,我们提供了一系列教程,以便您可以开发自己的基于Docker的应用程序。 我们涵盖了广泛的主题,从通过命令行的Docker到开发,测试,部署和持续集成。 通过我们简单易懂的教程,您将能够在最短的时间内启动并运行自己的项目。 在这里查看 !
目录
-
1.简介 2.开始之前 3.在RAM中存储数据 4.场景 5.测试容器 6. Arquillian 7.阴 8. Docker撰写规则 9.结论 10.下一步是什么
1.简介
如果我们考虑最明显地受到Docker和基于容器的虚拟化的影响的软件工程领域,那么测试和测试自动化无疑是其中之一。 随着软件系统变得越来越复杂,它们所基于的软件堆栈也越来越复杂,涉及许多活动部件。
在本教程的这一部分中,我们将学习两个框架,这些框架通过利用Docker及其周围的工具极大地简化了基于JVM的应用程序的集成,组件和端到端测试。
2.开始之前
在本教程的第二部分中,我们简要提到了作为Docker容器部署的Java应用程序应至少使用Java 8 update 131
或更高版本,但是我们还没有机会详细说明这一事实。
为了理解这个问题,让我们集中讨论JVM如何处理Java应用程序的两个最重要的资源:内存(堆)和CPU。 为了便于说明,我们将Docker安装在专用虚拟机上,该虚拟机分配了2 CPU cores
和4GB of memory
。 记住这一配置,让我们从JVM的角度来看一下:
$ docker run -- rm openjdk:8-jdk-alpine java -XshowSettings:vm -XX:+UseParallelGC -XX:+PrintFlagsFinal –version
控制台中将打印出很多信息,但是对我们来说最有趣的部分是ParallelGCThreads
和VM settings
。
... uintx ParallelGCThreads = 2 ... VM settings:
Max. Heap Size (Estimated): 992.00M
Ergonomics Machine Class: server
Using VM: OpenJDK 64-Bit Server VM openjdk version "1.8.0_131" OpenJDK Runtime Environment (IcedTea 3.4.0) (Alpine 8.131.11-r2) OpenJDK 64-Bit Server VM (build 25.131-b11, mixed mode)
在少于(或等于)8个内核的机器上, ParallelGCThreads
等于内核数 ,而堆的默认最大限制约为可用物理内存的25% 。 很酷,到目前为止,JVM报告的数字是有意义的。
让我们进一步推动它,并使用Docker资源管理功能将容器CPU和内存使用量分别限制为1 core
和256Mb
。
$ docker run -- --cpuset-cpus=0 --memory=256m openjdk:8-jdk-alpine java -XshowSettings:vm -XX:+UseParallelGC -XX:+PrintFlagsFinal -version $ docker run -- rm --cpuset-cpus=0 --memory=256m openjdk:8-jdk-alpine java -XshowSettings:vm -XX:+UseParallelGC -XX:+PrintFlagsFinal -version
这次图片有点不同:
... uintx ParallelGCThreads = 1 ... VM settings:
Max. Heap Size (Estimated): 992.00M
Ergonomics Machine Class: client
Using VM: OpenJDK 64-Bit Server VM
JVM能够相应地调整ParallelGCThreads
,但似乎完全忽略了内存限制。 没错,要使JVM了解容器化环境,我们需要解锁实验性JVM选项: -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
。
$ docker run -- --cpuset-cpus=0 --memory=256m openjdk:8-jdk-alpine java -XshowSettings:vm -XX:+UseParallelGC -XX:+PrintFlagsFinal -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap –version $ docker run -- rm --cpuset-cpus=0 --memory=256m openjdk:8-jdk-alpine java -XshowSettings:vm -XX:+UseParallelGC -XX:+PrintFlagsFinal -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap –version
现在结果看起来好多了:
... uintx ParallelGCThreads = 1 ... VM settings:
Max. Heap Size (Estimated): 112.00M
Ergonomics Machine Class: client
Using VM: OpenJDK 64-Bit Server VM
好奇的读者可能想知道为什么最大堆大小大大超过分配给容器的可用物理内存的预期25%
。 答案是Docker还为交换分配了内存(如果未指定,则等于所需的内存限制),因此JVM看到的真实值为256Mb + 256Mb = 512Mb
。
但是即使在这里,我们也可以进行改进。 通常,当我们在Docker容器中运行Java应用程序时,其中仅存在单个JVM进程的位置,那么为什么不给它所有可用的内存容器呢? 通过添加-XX:MaxRAMFraction=1
命令行选项,实际上非常简单。
$ docker run -- --cpuset-cpus=0 --memory=256m openjdk:8-jdk-alpine java -XshowSettings:vm -XX:+UseParallelGC -XX:+PrintFlagsFinal -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1 -version $ docker run -- rm --cpuset-cpus=0 --memory=256m openjdk:8-jdk-alpine java -XshowSettings:vm -XX:+UseParallelGC -XX:+PrintFlagsFinal -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1 -version
让我们检查一下JVM报告:
... uintx ParallelGCThreads = 1 ... VM settings:
Max. Heap Size (Estimated): 228.00M
Ergonomics Machine Class: client
Using VM: OpenJDK 64-Bit Server VM openjdk version "1.8.0_131" OpenJDK Runtime Environment (IcedTea 3.4.0) (Alpine 8.131.11-r2) OpenJDK 64-Bit Server VM (build 25.131-b11, mixed mode)
看起来很完美。 请注意这个试验性的JVM选项并适当地使用它们,以便您可以调整Java应用程序应使用的资源。
3.在RAM中存储数据
应用程序很少依赖某种数据存储(有时甚至不止一个)的情况很少。 而且,毫不奇怪,大多数此类存储将数据保留在某种持久性存储上。 确实确实很棒,但是这种数据存储的存在常常使测试成为挑战,特别是在召集数据级别的测试用例隔离时。
您可能已经猜到了, Docker可以极大地减少创建可再现测试场景所需的工作。 但是,数据存储仍然是整个测试执行时间的主要贡献者。
稍后我们将看到Docker有一些帮助我们的东西: tmpfs 。 它有效地为容器提供了一种管理数据的方法,而无需将其永久写入任何位置,而将其保留在主机的内存中(如果内存不足,则可以交换)。
有多个选项可以将tmpfs支持的卷附加到您的容器,但是最近推荐的一种方法是使用--mount
命令行参数(如果是docker-compose ,则使用规范的tmpfs部分)。
docker run -- rm -d \
--name mysql \
-- mount type =tmpfs,destination= /var/lib/mysql \
-e MYSQL_ROOT_PASSWORD= 'p$ssw0rd' \
-e MYSQL_DATABASE=my_app_db \
-e MYSQL_ROOT_HOST=% \
mysql:8.0.2
测试执行的速度提高可能是巨大的,但是永远不要在生产中(并且很可能在任何其他环境中)使用此功能,因为当容器停止时, tmpfs安装会消失。 即使已提交容器,也不会保存tmpfs安装。
4.场景
掌握了这些技巧和窍门,我们可以讨论将要实现的测试方案。 从本质上讲,对本教程前面部分开发的应用程序之一进行端到端测试将是很棒的。 测试方案的最终目标是验证用于任务管理的REST(ful)API是否可用并返回预期结果。
从部署堆栈的角度来看,这意味着我们必须拥有MySQL实例和应用程序实例(让我们选择之前开发的基于Spring Boot的实例,但这并不重要)。 该应用程序公开了REST(ful)API,并且应该能够与MySQL实例进行通信。 当然,我们希望这些实例作为Docker容器存在。
测试场景将使用出色的REST保证框架进行测试支架调用任务管理REST(ful)API,以获取所有可用任务的列表。 另外,我们所有的测试场景都将基于JUnit框架。
5.测试容器
我们要看的第一个框架是TestContainers ,这是一个Java库,支持JUnit测试,并提供常见数据库,Selenium Web浏览器或其他可以在Docker容器中运行的轻巧,一次性的实例。
让我们看看如何使用TestContainers将之前概述的测试方案投影到正在运行的JUnit测试中,以使我们受益。 最后,它看起来非常简单。
package com.javacodegeeks; io.restassured.RestAssured.when; import static io.restassured.RestAssured.when; import static org.hamcrest.CoreMatchers.equalTo; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.Network; import org.testcontainers.containers.wait.HostPortWaitStrategy; import io.restassured.RestAssured; public class SpringBootAppIntegrationTest {
@ClassRule
public static Network network = Network.newNetwork();
@ClassRule
public static GenericContainer> mysql = new
GenericContainer<>( "mysql:8.0.2" )
.withEnv( "MYSQL_ROOT_PASSWORD" , "p$ssw0rd" )
.withEnv( "MYSQL_DATABASE" , "my_app_db" )
.withEnv( "MYSQL_ROOT_HOST" , "%" )
.withExposedPorts( 3306 )
.withNetwork(network)
.withNetworkAliases( "mysql" )
.waitingFor( new HostPortWaitStrategy());
@ClassRule
public static GenericContainer> javaApp =
new GenericContainer<>( "jcg/spring-boot-webapp:latest" )
.withEnv( "DB_HOST" , "mysql" )
.withExposedPorts( 19900 )
.withStartupAttempts( 3 )
.withNetwork(network)
.waitingFor( new HostPortWaitStrategy());
@BeforeClass
public static void setUp() {
RestAssured.baseURI = " http:// " + javaApp.getContainerIpAddress();
RestAssured.port = javaApp.getMappedPort( 19900 );
}
@Test
public void getAllTasks() {
when()
.get( "/tasks" )
.then()
.statusCode( 200 )
.body(equalTo( "[]" ));
} }
TestContainers带有一组预定义的专用容器(用于MySQL , PostgreSQL , Oracle XE和其他容器),但是您始终可以选择使用自己的容器(就像我们在上面的代码片段中所做的那样)。 开箱即用也支持Docker Compose规范。
如果您使用Windows作为开发平台,请注意, TestContainers未在Windows上进行定期测试。 不过,如果您想尝试一下,目前的建议是使用alpha release 。
6. Arquillian
列表中的下一个是Arquillian ,这是一个针对JVM的创新且高度可扩展的测试平台,使开发人员能够轻松地为Java中间件创建自动集成,功能和验收测试。 与框架相比, Arquillian实际上更是一个测试平台,其中Docker支持只是众多可用选项之一。
让我们看一下上一节中看到的相同测试用例在转换为Arquillian Cube之后的外观 , Arquillian Cube是Arquillian扩展,可用于管理Arquillian中的 Docker容器。
public class SpringBootAppIntegrationTest {
@ClassRule
public static NetworkDslRule network = new NetworkDslRule( "my-app-network" );
@ClassRule
public static ContainerDslRule mysql =
new ContainerDslRule( "mysql:8.0.2" , "mysql" )
.withEnvironment( "MYSQL_ROOT_PASSWORD" , "p$ssw0rd" )
.withEnvironment( "MYSQL_DATABASE" , "my_app_db" )
.withEnvironment( "MYSQL_ROOT_HOST" , "%" )
.withExposedPorts( 3306 )
.withNetworkMode( "my-app-network" )
.withAwaitStrategy(AwaitBuilder.logAwait( "/usr/sbin/mysqld: ready for connections" ));
@ClassRule
public static ContainerDslRule javaApp =
new ContainerDslRule( "jcg/spring-boot-webapp:latest" , "spring-boot-webapp*" )
.withEnvironment( "DB_HOST" , "mysql" )
.withPortBinding( 19900 )
.withNetworkMode( "my-app-network" )
.withLink( "mysql" , "mysql" )
.withAwaitStrategy(AwaitBuilder.logAwait( "Started AppStarter" ));
@BeforeClass
public static void setUp() {
RestAssured.baseURI = " http:// " + javaApp.getIpAddress();
RestAssured.port = javaApp.getBindPort( 19900 );
}
@Test
public void getAllTasks() {
when()
.get( "/tasks" )
.then()
.statusCode( 200 )
.body(equalTo( "[]" ));
} }
与TestContainers有一些区别,但总的来说,测试用例应该已经看起来很熟悉。 目前(至少在使用Container Objects DSL时 ) Arquillian Cube中似乎不可用的关键功能是对网络别名的支持,因此我们必须退回到传统的容器链接 ,才能将Spring Boot应用程序与MySQL连接后端。
值得一提的是, Arquillian Cube有很多其他方法可以从测试场景管理Docker容器,包括Docker Compose规范支持。 附带说明一下,围绕Arquillian的开发速度简直令人难以置信,它有机会成为现代Java应用程序的一站式测试平台。
7.阴
Overcast是XebiaLabs的Java库,用于对云中的主机进行测试。 Overcast是最早提供广泛Docker支持的测试框架之一,同时旨在实现更雄心勃勃的目标,以在测试场景中抽象化主机管理。 它具有许多非常有趣的功能,但遗憾的是似乎没有积极维护,其最新版本为2015年。尽管如此,它还是值得一看的,因为它具有一些独特的价值。
Overcast使用配置驱动的方法来描述测试中的主机,这是由于它并不仅仅针对容器或Docker 。
mysql {
name= "mysql"
dockerImage= "mysql:8.0.2"
remove= true
removeVolume= true
env =[ "MYSQL_ROOT_PASSWORD=p$ssw0rd" , "MYSQL_DATABASE=my_app_db" , "MYSQL_ROOT_HOST=%" ] } spring-boot-webapp {
dockerImage= "jcg/spring-boot-webapp:latest"
exposeAllPorts= true
remove= true
removeVolume= true
env =[ "DB_HOST=mysql" ]
links=[ "mysql:mysql" ] }
有了这样的配置(存储在overcast.conf
,我们可以通过测试场景中的容器名称来引用它们。
public class SpringBootAppIntegrationTest {
private static CloudHost mysql = CloudHostFactory.getCloudHost( "mysql" );
private static CloudHost javaApp = CloudHostFactory.getCloudHost( "spring-boot-webapp" );
@BeforeClass
public static void setUp() {
mysql.setup();
javaApp.setup();
RestAssured.baseURI = " http:// " + javaApp.getHostName();
RestAssured.port = javaApp.getPort( 19900 );
await()
.atMost( 20 , TimeUnit.SECONDS)
.ignoreExceptions()
.pollInterval( 1 , TimeUnit.SECONDS)
.untilAsserted(() ->
when()
.get( "/application/health" )
.then()
.statusCode( 200 ));
}
@AfterClass
public static void tearDown() {
javaApp.teardown();
mysql.teardown();
}
@Test
public void getAllTasks() {
when()
.get( "/tasks" )
.then()
.statusCode( 200 )
.body(equalTo( "[]" ));
} }
除了运行状况检查部分,它看起来非常简单。 覆盖不提供验证容器是否已准备就绪并可以满足请求的方法,因此我们采用了另一个出色的库Awaitility来弥补这一点。
Overcast可以提供许多潜力和利益,希望我们有一天能将其恢复到活跃的开发中。 对于此示例,为了使用最新的“ 覆盖”功能,我们必须从源代码构建它。
8. Docker撰写规则
最后但并非最不重要的一点,让我们讨论Docker Compose Rule ,这是Palantir Technologies的一个库,用于执行与Docker Compose管理的容器进行交互的JUnit测试。 在后台,它使用docker-compose命令行工具,到目前为止,不支持Windows平台。
您可能会猜到,我们必须从docker-compose.yml
规范开始,以便稍后可以由Docker Compose Rule读取。 这是一个例子。
version: '2.1' services:
mysql:
image: mysql:8.0.2
environment:
- MYSQL_ROOT_PASSWORD=p$ssw0rd
- MYSQL_DATABASE=my_app_db
- MYSQL_ROOT_HOST=%
expose:
- 3306
healthcheck:
test : [ "CMD-SHELL" , "ss -ltn src :3306 | grep 3306" ]
interval: 10s
timeout: 5s
retries: 3
tmpfs:
- /var/lib/mysql
networks:
- my-app-network
java-app:
image: jcg /spring-boot-webapp :latest
environment:
- DB_HOST=mysql
ports:
- 19900
depends_on:
mysql:
condition: service_healthy
networks:
- my-app-network networks:
my-app-network:
driver: bridge
这样,我们只需要将此规范提供给Docker Compose Rule即可 ,如下面的代码片段所示。 另请注意,我们已经通过运行状况检查增强了该方案,以确保我们的Spring Boot应用程序已完全启动。
public class SpringBootAppIntegrationTest {
@ClassRule
public static DockerComposeRule docker = DockerComposeRule.builder()
.file( "src/test/resources/docker-compose.yml" )
.waitingForService( "java-app" ,
HealthChecks.toRespond2xxOverHttp( 19900 , (port) ->
port.inFormat( " http:// $HOST:$EXTERNAL_PORT/application/health" )),
Duration.standardSeconds( 30 )
)
.shutdownStrategy(ShutdownStrategy.GRACEFUL)
.build();
@BeforeClass
public static void setUp() {
final DockerPort port = docker.containers().container( "java-app" ).port( 19900 );
RestAssured.baseURI = port.inFormat( " http:// $HOST" );
RestAssured.port = port.getExternalPort();
}
@Test
public void getAllTasks() {
when()
.get( "/tasks" )
.then()
.statusCode( 200 )
.body(equalTo( "[]" ));
} }
这可能是将Docker集成到JUnit测试方案中的最简单方法。 而且,巧合的是,相同的Docker Compose规范可以共享并重用于其他目的。
9.结论
在本教程的这一部分中,我们讨论了Docker如何颠覆并真正改变了我们在实际项目中使用的一些最复杂的测试策略。 正如我们通过熟悉不同的Java测试框架所看到的那样,对Docker的支持非常出色,但是根据您所依赖的操作系统和Docker安装,为您的项目选择合适的工具可能是一个挑战。
然而,强大的力量伴随着巨大的责任。 Docker不是灵丹妙药,请始终牢记测试金字塔附近,并尝试找到最适合您的平衡点。
10.下一步是什么
在本教程的下一部分中,我们将讨论很多典型的部署流程,其中Docker容器(和一般而言的容器)是一等公民。
完整的项目资源可供下载 。
翻译自: https://www.javacodegeeks.com/2018/01/docker-java-developers-test-docker.html