Статьи

Docker для разработчиков Java: тест на Docker

Эта статья является частью нашего курса Академии под названием 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.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. Тем не менее, если вы хотите попробовать его, на данный момент рекомендуется использовать альфа-релиз .

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.baseURI = "http://" + javaApp.getIpAddress();
        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.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("[]"));
    }
}

Это выглядит довольно просто, за исключением проверки здоровья. 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) ->
                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, на которые вы полагаетесь.

Однако с великой силой приходит большая ответственность. Докер — не серебряная пуля, всегда помните о тестовой пирамиде рядом и постарайтесь найти баланс, который работает для вас лучше всего.

10. Что дальше

В следующем разделе руководства мы поговорим о нескольких типичных системах развертывания, в которых контейнеры Docker (и контейнеры в целом) являются гражданами первого класса.

Полные исходные коды проекта доступны для скачивания .