Эта статья является частью нашего курса Академии под названием Docker Tutorial для разработчиков Java .
В этом курсе мы предлагаем серию руководств, чтобы вы могли разрабатывать свои собственные приложения на основе Docker. Мы охватываем широкий спектр тем, от Docker через командную строку, до разработки, тестирования, развертывания и непрерывной интеграции. С нашими простыми учебными пособиями вы сможете запустить и запустить собственные проекты за минимальное время. Проверьте это здесь !
Содержание
1. Введение
Если мы подумаем об областях разработки программного обеспечения, где влияние Docker и контейнерной виртуализации наиболее заметно, то тестирование и автоматизация тестирования, безусловно, являются одними из них. По мере того, как программные системы становятся все более и более сложными, стеки программного обеспечения, на которых они построены, также включают множество движущихся частей.
В этом разделе руководства мы собираемся изучить пару платформ, которые чрезвычайно упрощают интеграцию, компонентное и сквозное тестирование приложений на основе JVM, используя Docker и инструменты вокруг него.
2. Прежде чем начать
Во второй части этого руководства мы кратко упомянули, что приложения Java, развернутые в виде контейнеров Docker, должны использовать по крайней мере Java 8 update 131
или более поздней Java 8 update 131
но у нас пока не было возможности уточнить важность этого факта.
Чтобы понять проблему, давайте сосредоточимся на том, как JVM работает с двумя наиболее важными ресурсами для приложений Java: памятью (куча) и процессором. В целях иллюстрации, мы собираемся установить Docker на выделенную виртуальную машину, которая имеет 2 CPU cores
и 4GB of memory
выделенной 4GB of memory
. Имея в виду эту конфигурацию, давайте посмотрим на это с точки зрения JVM:
1
|
$ docker run -- rm openjdk:8-jdk-alpine java -XshowSettings:vm -XX:+UseParallelGC -XX:+PrintFlagsFinal –version |
В консоли будет напечатано много информации, но наиболее интересными для нас являются VM settings
ParallelGCThreads
и VM settings
.
01
02
03
04
05
06
07
08
09
10
11
12
|
... 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 , чтобы ограничить использование ЦП и памяти контейнера до 1 core
и 256Mb
соответственно.
1
|
$ docker run -- rm --cpuset-cpus=0 --memory=256m openjdk:8-jdk-alpine java -XshowSettings:vm -XX:+UseParallelGC -XX:+PrintFlagsFinal -version |
На этот раз картина немного другая:
1
2
3
4
5
6
7
8
|
... 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
.
1
|
$ docker run -- rm --cpuset-cpus=0 --memory=256m openjdk:8-jdk-alpine java -XshowSettings:vm -XX:+UseParallelGC -XX:+PrintFlagsFinal -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap –version |
Результаты теперь выглядят намного лучше:
1
2
3
4
5
6
7
8
|
... 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
.
Но даже здесь мы могли бы сделать улучшения. Обычно, когда мы выполняем приложения Java внутри контейнеров Docker , там есть место только для одного процесса JVM, так почему бы нам просто не отдать ему все доступное содержимое контейнера? Это на самом деле очень просто сделать, добавив -XX:MaxRAMFraction=1
командной строки -XX:MaxRAMFraction=1
.
1
|
$ 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:
01
02
03
04
05
06
07
08
09
10
11
12
|
... 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. Хранение данных в оперативной памяти
Редко бывает, что приложение не зависит от какого-либо хранилища данных (иногда даже более одного). И, что неудивительно, большинство таких хранилищ хранят данные на каком-то долговременном хранилище. Это действительно здорово, но довольно часто наличие таких хранилищ данных затрудняет тестирование, особенно при вызове изоляции тестовых случаев на уровне данных.
Возможно, вы уже догадались, что Docker мог бы значительно сократить усилия, необходимые для создания воспроизводимых тестовых сценариев. Но все же хранилища данных служат основным фактором, влияющим на общее время выполнения теста.
Как мы сейчас увидим, у Docker есть что-то, что может нам помочь: tmpfs . Это эффективно дает контейнеру способ управлять данными, не записывая их в постоянное хранилище, сохраняя их в памяти хост-машины (или подкачку, если памяти мало).
Существует несколько доступных опций, которые позволяют присоединять тома с поддержкой tmpfs к вашему контейнеру, но в наши дни рекомендуется использовать --mount
командной строки --mount
(или раздел спецификации tmpfs в случае docker-compose ).
1
2
3
4
5
6
7
|
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. Сценарий
Вооружившись этими уловками и советами, мы могли бы обсудить тестовый сценарий, который мы собираемся реализовать. По сути, было бы здорово запустить сквозное тестирование для одного из приложений, которые мы разработали в предыдущих частях учебника. Конечной целью сценария тестирования будет проверка доступности API-интерфейсов REST (ful) для управления задачами и получение ожидаемых результатов.
С точки зрения стека развертывания это означает, что у нас должен быть экземпляр MySQL и экземпляр приложения (давайте выберем тот, который был разработан на основе Spring Boot, который мы разработали ранее, но это не имеет большого значения). Это приложение предоставляет REST (ful) API и должно иметь возможность взаимодействовать с экземпляром MySQL . Конечно, мы бы хотели, чтобы эти экземпляры жили как контейнеры Docker .
Тестовый сценарий будет вызывать API-интерфейсы REST (ful) для управления задачами, чтобы получить список всех доступных задач, используя потрясающую гарантированную REST- среду для тестирования лесов. Кроме того, все наши тестовые сценарии будут основаны на фреймворке JUnit .
5. Тестконтейнеры
Первой платформой, которую мы рассмотрим, является TestContainers , библиотека Java, которая поддерживает тесты JUnit и предоставляет легковесные, одноразовые экземпляры общих баз данных, веб-браузеры Selenium или все, что может работать в контейнере Docker .
Давайте посмотрим, как мы могли бы спроецировать описанный выше сценарий тестирования на запуск теста JUnit, используя TestContainers для наших преимуществ. В конце это выглядит довольно просто.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
package com.javacodegeeks; 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.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. Тем не менее, если вы хотите попробовать его, на данный момент рекомендуется использовать альфа-релиз .
6. Аркиллиан
Следующей в списке является Arquillian , инновационная и расширяемая платформа тестирования для JVM, которая позволяет разработчикам легко создавать автоматизированные интеграционные, функциональные и приемочные тесты для промежуточного программного обеспечения Java. Arquillian действительно является скорее платформой для тестирования, чем фреймворком, где поддержка Docker является лишь одним из многих доступных вариантов.
Давайте посмотрим, как может выглядеть тот же тест, который мы видели в предыдущем разделе, при переводе в Arquillian Cube , расширение Arquillian, которое можно использовать для управления контейнерами Docker из Arquillian .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
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.port = javaApp.getBindPort( 19900 ); } @Test public void getAllTasks() { when() .get( "/tasks" ) .then() .statusCode( 200 ) .body(equalTo( "[]" )); } } |
Есть несколько отличий от TestContainers, но, по большому счету , тестовый пример должен выглядеть уже знакомым. Ключевой особенностью, которая на данный момент недоступна в Arquillian Cube (по крайней мере, при использовании объектов-контейнеров DSL ), является поддержка псевдонимов в сети, поэтому нам пришлось обратиться к устаревшим ссылкам на контейнеры , чтобы подключить наше приложение Spring Boot к MySQL. бэкенд.
Стоит отметить, что в Arquillian Cube есть несколько альтернативных способов управления контейнерами Docker из тестовых сценариев, включая поддержку спецификаций Docker Compose . С другой стороны, скорость разработки вокруг Arquillian просто невероятна, у нее есть все шансы стать универсальной платформой для тестирования современных приложений Java.
7. Пасмурно
Overcast — это библиотека Java от XebiaLabs, предназначенная для тестирования на хостах в облаке. Overcast была одной из первых тестовых сред, которая предложила обширную поддержку Docker , стремясь к более амбициозным целям абстрагирования управления хостом в тестовых сценариях. У него много очень интересных функций, но, к сожалению, похоже, что он больше не поддерживается, с последним выпуском от 2015 года. Тем не менее, стоит посмотреть, поскольку он имеет некоторые уникальные ценности.
В Overcast используется подход, основанный на конфигурации, для описания тестируемых хостов, обусловленный тем, что он ориентирован не только на контейнеры и / или Docker .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
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
, мы можем ссылаться на контейнеры по их именам из тестовых сценариев.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
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.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( "[]" )); } } |
Это выглядит довольно просто, за исключением проверки здоровья. Overcast не предоставляет средств для проверки того, что контейнеры готовы и готовы к обслуживанию запросов, поэтому мы добавили еще одну замечательную библиотеку Awaitility , чтобы компенсировать это.
У Overcast есть много потенциальных возможностей и преимуществ, надеюсь, мы когда-нибудь вернемся к активной разработке. Что касается этого примера, чтобы использовать новейшие функции Overcast, мы должны были собрать его из исходников.
8. Правило составления Docker
И последнее, но не менее важное: давайте обсудим правило Docker Compose , библиотеку Palantir Technologies для выполнения тестов JUnit, которые взаимодействуют с контейнерами, управляемыми Docker Compose . Под капотом используется инструмент командной строки docker-compose , и на данный момент платформа Windows не поддерживается.
Как вы можете догадаться, мы должны начинать со спецификации docker-compose.yml
чтобы потом ее можно было прочитать с помощью Docker Compose Rule . Вот пример одного.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
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 полностью запущено.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
public class SpringBootAppIntegrationTest { @ClassRule public static DockerComposeRule docker = DockerComposeRule.builder() .file( "src/test/resources/docker-compose.yml" ) .waitingForService( "java-app" , HealthChecks.toRespond2xxOverHttp( 19900 , (port) -> Duration.standardSeconds( 30 ) ) .shutdownStrategy(ShutdownStrategy.GRACEFUL) .build(); @BeforeClass public static void setUp() { final DockerPort port = docker.containers().container( "java-app" ).port( 19900 ); 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, на которые вы полагаетесь.
Однако с великой силой приходит большая ответственность. Докер — не серебряная пуля, всегда помните о тестовой пирамиде рядом и постарайтесь найти баланс, который работает для вас лучше всего.
10. Что дальше
В следующем разделе руководства мы поговорим о нескольких типичных системах развертывания, в которых контейнеры Docker (и контейнеры в целом) являются гражданами первого класса.
Полные исходные коды проекта доступны для скачивания .