Использование Testcontainers радикально улучшило процесс работы с тестовыми сценариями. Благодаря этому инструменту создание сред для интеграционных тестов стало проще (см. статью Изоляция при тестировании с помощью Kafka ). Теперь мы можем легко запускать контейнеры с разными версиями баз данных, брокеров сообщений и других сервисов. Для интеграционных тестов Testcontainers оказались незаменимыми.
Хотя нагрузочное тестирование встречается реже, чем функциональное тестирование, оно может быть гораздо более приятным. Изучение графиков и анализ работы того или иного сервиса могут принести настоящее удовольствие. Такие задачи встречаются редко, но для меня они особенно увлекательны.
Цель этой статьи — продемонстрировать подход к созданию установки для нагрузочного тестирования так же, как пишутся обычные интеграционные тесты: в виде тестов Spock с использованием Testcontainers в среде проекта Gradle. Используются утилиты нагрузочного тестирования, такие как Gatling, WRK и Яндекс.Танк.
Набор инструментов: Gradle + Spock Framework + Testcontainers. Вариант реализации — отдельный модуль Gradle. Используются утилиты нагрузочного тестирования Gatling, WRK и Яндекс.Танк.
Существует два подхода к работе с тест-объектом:
В первом случае у нас есть набор нагрузочных тестов, не зависящих от версии проекта и изменений. Этот подход легче поддерживать в будущем, но он ограничивается тестированием только опубликованных изображений. Мы, конечно, можем вручную создавать эти изображения локально, но это менее автоматизировано и снижает воспроизводимость. При запуске в CI/CD без необходимых образов тесты пройдут неудачно.
Во втором случае тесты запускаются на последней версии сервиса. Это позволяет интегрировать нагрузочные тесты в CI и получать данные об изменениях производительности между версиями сервиса. Однако нагрузочные тесты обычно занимают больше времени, чем модульные тесты. Решение о включении таких тестов в CI как часть контроля качества остается за вами.
В данной статье рассмотрен первый подход. Благодаря Споку мы можем запускать тесты на нескольких версиях сервиса для сравнительного анализа:
where: image | _ 'avvero/sandbox:1.0.0' | _ 'avvero/sandbox:1.1.0' | _
Важно отметить, что цель этой статьи — продемонстрировать организацию пространства тестирования, а не полномасштабное нагрузочное тестирование.
В качестве объекта тестирования возьмем простой HTTP-сервис с именем Sandbox, который публикует конечную точку и использует данные из внешнего источника для обработки запросов. У сервиса есть база данных.
Исходный код сервиса, включая Dockerfile, доступен в репозитории проекта Spring-sandbox .
Поскольку мы углубимся в детали далее в статье, я хочу начать с краткого обзора структуры модуля load-tests
Gradle, чтобы дать представление о его составе:
load-tests/ |-- src/ | |-- gatling/ | | |-- scala/ | | | |-- MainSimulation.scala # Main Gatling simulation file | | |-- resources/ | | | |-- gatling.conf # Gatling configuration file | | | |-- logback-test.xml # Logback configuration for testing | |-- test/ | | |-- groovy/ | | | |-- pw.avvero.spring.sandbox/ | | | | |-- GatlingTests.groovy # Gatling load test file | | | | |-- WrkTests.groovy # Wrk load test file | | | | |-- YandexTankTests.groovy # Yandex.Tank load test file | | |-- java/ | | | |-- pw.avvero.spring.sandbox/ | | | | |-- FileHeadLogConsumer.java # Helper class for logging to a file | | |-- resources/ | | | |-- wiremock/ | | | | |-- mappings/ # WireMock setup for mocking external services | | | | | |-- health.json | | | | | |-- forecast.json | | | |-- yandex-tank/ # Yandex.Tank load testing configuration | | | | |-- ammo.txt | | | | |-- load.yaml | | | | |-- make_ammo.py | | | |-- wrk/ # LuaJIT scripts for Wrk | | | | |-- scripts/ | | | | | |-- getForecast.lua |-- build.gradle
Репозиторий проекта — https://github.com/avvero/testing-bench .
Из описания выше мы видим, что у сервиса есть две зависимости: сервис https://external-weather-api.com и база данных. Их описание будет представлено ниже, но начнём с того, что включим взаимодействие всех компонентов схемы в среде Docker — опишем сеть:
def network = Network.newNetwork()
и предоставить сетевые псевдонимы для каждого компонента. Это чрезвычайно удобно и позволяет статически описывать параметры интегрирования.
Зависимости, такие как WireMock и утилиты нагрузочного тестирования, требуют настройки для работы. Это могут быть параметры, которые можно передать в контейнер, или целые файлы и каталоги, которые необходимо смонтировать в контейнеры.
Кроме того, нам необходимо получить результаты их работы из контейнеров. Для решения этих задач нам необходимо предоставить два набора каталогов:
workingDirectory
— каталог ресурсов модуля, непосредственно в load-tests/
.
reportDirectory
— каталог результатов работы, включая метрики и логи. Подробнее об этом будет в разделе об отчетах.Служба Sandbox использует Postgres в качестве базы данных. Опишем эту зависимость следующим образом:
def postgres = new PostgreSQLContainer<>("postgres:15-alpine") .withNetwork(network) .withNetworkAliases("postgres") .withUsername("sandbox") .withPassword("sandbox") .withDatabaseName("sandbox")
В объявлении указывается сетевой псевдоним postgres
, который служба Sandbox будет использовать для подключения к базе данных. Для завершения описания интеграции с базой данных сервису необходимо предоставить следующие параметры:
'spring.datasource.url' : 'jdbc:postgresql://postgres:5432/sandbox', 'spring.datasource.username' : 'sandbox', 'spring.datasource.password' : 'sandbox', 'spring.jpa.properties.hibernate.default_schema': 'sandbox'
Структурой базы данных управляет само приложение с помощью Flyway, поэтому никаких дополнительных манипуляций с базой данных в тесте не требуется.
Если у нас нет возможности, необходимости или желания запускать реальный компонент в контейнере, мы можем предоставить макет его API. Для сервиса https://external-weather-api.com используется WireMock.
Объявление контейнера WireMock будет выглядеть так:
def wiremock = new GenericContainer<>("wiremock/wiremock:3.5.4") .withNetwork(network) .withNetworkAliases("wiremock") .withFileSystemBind("${workingDirectory}/src/test/resources/wiremock/mappings", "/home/wiremock/mappings", READ_WRITE) .withCommand("--no-request-journal") .waitingFor(new LogMessageWaitStrategy().withRegEx(".*https://wiremock.io/cloud.*")) wiremock.start()
WireMock требует фиктивной конфигурации. Инструкция withFileSystemBind
описывает привязку файловой системы между путем к локальному файлу и путем внутри контейнера Docker. В этом случае каталог "${workingDirectory}/src/test/resources/wiremock/mappings"
на локальном компьютере будет смонтирован в /home/wiremock/mappings
внутри контейнера WireMock.
Ниже приведена дополнительная часть структуры проекта для понимания состава файлов в каталоге:
load-tests/ |-- src/ | |-- test/ | | |-- resources/ | | | |-- wiremock/ | | | | |-- mappings/ | | | | | |-- health.json | | | | | |-- forecast.json
Чтобы гарантировать, что макетные файлы конфигурации правильно загружены и приняты WireMock, вы можете использовать вспомогательный контейнер:
helper.execInContainer("wget", "-O", "-", "http://wiremock:8080/health").getStdout() == "Ok"
Вспомогательный контейнер описывается следующим образом:
def helper = new GenericContainer<>("alpine:3.17") .withNetwork(network) .withCommand("top")
Кстати, в IntelliJ IDEA версии 2024.1 появилась поддержка WireMock , а IDE предоставляет подсказки при формировании файлов макетной конфигурации.
Объявление сервисного контейнера Sandbox выглядит следующим образом:
def javaOpts = ' -Xloggc:/tmp/gc/gc.log -XX:+PrintGCDetails' + ' -XX:+UnlockDiagnosticVMOptions' + ' -XX:+FlightRecorder' + ' -XX:StartFlightRecording:settings=default,dumponexit=true,disk=true,duration=60s,filename=/tmp/jfr/flight.jfr' def sandbox = new GenericContainer<>(image) .withNetwork(network) .withNetworkAliases("sandbox") .withFileSystemBind("${reportDirectory}/logs", "/tmp/gc", READ_WRITE) .withFileSystemBind("${reportDirectory}/jfr", "/tmp/jfr", READ_WRITE) .withEnv([ 'JAVA_OPTS' : javaOpts, 'app.weather.url' : 'http://wiremock:8080', 'spring.datasource.url' : 'jdbc:postgresql://postgres:5432/sandbox', 'spring.datasource.username' : 'sandbox', 'spring.datasource.password' : 'sandbox', 'spring.jpa.properties.hibernate.default_schema': 'sandbox' ]) .waitingFor(new LogMessageWaitStrategy().withRegEx(".*Started SandboxApplication.*")) .withStartupTimeout(Duration.ofSeconds(10)) sandbox.start()
Известные параметры и настройки JVM включают:
Дополнительно настраиваются каталоги для сохранения результатов диагностики сервиса.
Если вам необходимо просмотреть логи какого-либо контейнера в файл, что, вероятно, понадобится на этапе написания и настройки тестового сценария, при описании контейнера можно использовать следующую инструкцию:
.withLogConsumer(new FileHeadLogConsumer("${reportDirectory}/logs/${alias}.log"))
В данном случае используется класс FileHeadLogConsumer
, который позволяет записывать в файл ограниченное количество логов. Это делается потому, что весь журнал, скорее всего, не понадобится в сценариях нагрузочного тестирования, а частичного журнала будет достаточно, чтобы оценить, правильно ли работает служба.
Существует множество инструментов для нагрузочного тестирования. В этой статье я предлагаю рассмотреть возможность использования трёх из них: Gatling, Wrk и Яндекс.Танк. Все три инструмента можно использовать независимо друг от друга.
Gatling — это инструмент нагрузочного тестирования с открытым исходным кодом, написанный на Scala. Он позволяет создавать сложные сценарии тестирования и предоставляет подробные отчеты. Основной файл моделирования Gatling подключается как ресурс Scala к модулю, что позволяет удобно работать с использованием всего спектра поддержки IntelliJ IDEA, включая подсветку синтаксиса и навигацию по методам для справки по документации.
Конфигурация контейнера для Гатлинга следующая:
def gatling = new GenericContainer<>("denvazh/gatling:3.2.1") .withNetwork(network) .withFileSystemBind("${reportDirectory}/gatling-results", "/opt/gatling/results", READ_WRITE) .withFileSystemBind("${workingDirectory}/src/gatling/scala", "/opt/gatling/user-files/simulations", READ_WRITE) .withFileSystemBind("${workingDirectory}/src/gatling/resources", "/opt/gatling/conf", READ_WRITE) .withEnv("SERVICE_URL", "http://sandbox:8080") .withCommand("-s", "MainSimulation") .waitingFor(new LogMessageWaitStrategy() .withRegEx(".*Please open the following file: /opt/gatling/results.*") .withStartupTimeout(Duration.ofSeconds(60L * 2)) ); gatling.start()
Настройка практически идентична другим контейнерам:
reportDirectory
.workingDirectory
.workingDirectory
.
Дополнительно в контейнер передаются параметры:
SERVICE_URL
со значением URL-адреса службы песочницы. Хотя, как упоминалось ранее, использование сетевых псевдонимов позволяет жестко закодировать URL-адрес непосредственно в коде сценария.
-s MainSimulation
для запуска конкретной симуляции.
Напоминаем структуру исходного файла проекта, чтобы понять, что и куда передается:
load-tests/ |-- src/ | |-- gatling/ | | |-- scala/ | | | |-- MainSimulation.scala # Main Gatling simulation file | | |-- resources/ | | | |-- gatling.conf # Gatling configuration file | | | |-- logback-test.xml # Logback configuration for testing
Поскольку это конечный контейнер, и мы ожидаем получить результаты после его завершения, мы устанавливаем ожидание .withRegEx(".*Please open the following file: /opt/gatling/results.*")
. Тест завершится, когда это сообщение появится в логах контейнера или через 60 * 2
секунды.
Я не буду углубляться в DSL сценариев этого инструмента. Посмотреть код использованного сценария можно в репозитории проекта .
Wrk — простой и быстрый инструмент нагрузочного тестирования. Он может генерировать значительную нагрузку при минимальных ресурсах. Ключевые особенности включают в себя:
Конфигурация контейнера для Wrk следующая:
def wrk = new GenericContainer<>("ruslanys/wrk") .withNetwork(network) .withFileSystemBind("${workingDirectory}/src/test/resources/wrk/scripts", "/tmp/scripts", READ_WRITE) .withCommand("-t10", "-c10", "-d60s", "--latency", "-s", "/tmp/scripts/getForecast.lua", "http://sandbox:8080/weather/getForecast") .waitingFor(new LogMessageWaitStrategy() .withRegEx(".*Transfer/sec.*") .withStartupTimeout(Duration.ofSeconds(60L * 2)) ) wrk.start()
Чтобы Wrk работал с запросами к сервису Sandbox, необходимо описание запроса через Lua-скрипт, поэтому монтируем каталог скриптов из workingDirectory
. С помощью команды запускаем Wrk, указав скрипт и URL-адрес целевого метода сервиса. Wrk записывает отчет в журнал на основе своих результатов, который можно использовать для определения ожиданий.
Яндекс.Танк — инструмент нагрузочного тестирования, разработанный Яндексом. Он поддерживает различные механизмы нагрузочного тестирования, такие как JMeter и Phantom. Для хранения и отображения результатов нагрузочного тестирования можно использовать бесплатный сервис Overload .
Вот конфигурация контейнера:
copyFiles("${workingDirectory}/src/test/resources/yandex-tank", "${reportDirectory}/yandex-tank") def tank = new GenericContainer<>("yandex/yandex-tank") .withNetwork(network) .withFileSystemBind("${reportDirectory}/yandex-tank", "/var/loadtest", READ_WRITE) .waitingFor(new LogMessageWaitStrategy() .withRegEx(".*Phantom done its work.*") .withStartupTimeout(Duration.ofSeconds(60L * 2)) ) tank.start()
Конфигурация нагрузочного тестирования для Sandbox представлена двумя файлами: load.yaml
и ammo.txt
. В составе описания контейнера файлы конфигурации копируются в reportDirectory
, который будет смонтирован как рабочий каталог. Вот структура исходных файлов проекта, чтобы понять, что и куда передается:
load-tests/ |-- src/ | |-- test/ | | |-- resources/ | | | |-- yandex-tank/ | | | | |-- ammo.txt | | | | |-- load.yaml | | | | |-- make_ammo.py
Результаты тестов, включая записи и журналы производительности JVM, сохраняются в каталоге build/${timestamp}
, где ${timestamp}
представляет собой временную метку каждого запуска теста.
Для просмотра будут доступны следующие отчеты:
Если использовался Гатлинг:
Если использовался Wrk:
Если использовался Яндекс.Танк:
Структура каталогов для отчетов следующая:
load-tests/ |-- build/ | |-- ${timestamp}/ | | |-- gatling-results/ | | |-- jfr/ | | |-- yandex-tank/ | | |-- logs/ | | | |-- sandbox.log | | | |-- gatling.log | | | |-- gc.log | | | |-- wiremock.log | | | |-- wrk.log | | | |-- yandex-tank.log | |-- ${timestamp}/ | |-- ...
Нагрузочное тестирование — важнейший этап жизненного цикла разработки программного обеспечения. Он помогает оценить производительность и стабильность приложения при различных условиях нагрузки. В этой статье представлен подход к созданию среды нагрузочного тестирования с использованием Testcontainers, который позволяет легко и эффективно настроить среду тестирования.
Тестовые контейнеры существенно упрощают создание сред для интеграционных тестов, обеспечивая гибкость и изоляцию. Для нагрузочного тестирования этот инструмент позволяет развертывать необходимые контейнеры с различными версиями сервисов и баз данных, что упрощает проведение тестов и улучшает воспроизводимость результатов.
Предоставленные примеры конфигурации Gatling, Wrk и Яндекс.Танка, а также настройки контейнера демонстрируют, как эффективно интегрировать различные инструменты и управлять параметрами тестирования.
Дополнительно был описан процесс логирования и сохранения результатов тестирования, необходимый для анализа и улучшения производительности приложения. Этот подход может быть расширен в будущем для поддержки более сложных сценариев и интеграции с другими инструментами мониторинга и анализа.
Спасибо за внимание к этой статье и удачи в написании полезных тестов!