На Хабре совсем нет информации про TestContainers. На момент написания этой статьи, в поисковой выдаче есть анонсы наших же конференций, и всё. Между тем, в проекте на GitHub у них уже более 700 коммитов, 54 контрибьютора и 5 лет истории. Похоже, все эти пять лет проект тщательно скрывался спецслужбами и НЛО. Настало время выйти из тени на свет.
Чукча — читатель, а не писатель. Поэтому, вместо написания своего текста, я попросил разрешения на перевод соответствующей статьи из блога RebelLabs.
Итак, здесь мы поделимся парой слов о наимоднейшей Java-библиотеке для интеграционного тестирования — TestContainers. Кроме этого, будет немного о том, почему интеграционное тестирование настолько важно для ZeroTurnaround и их требования к интеграционным тестам. И конечно, будет полнофункциональный пример интеграционного теста для Java-агента. Если кто-то никогда в глаза не видел код Java-агента, то сейчас самое время. Добро пожаловать под кат!
Интеграционное тестирование в компании ZeroTurnaround
Продукты компании ZeroTurnaround интегрируются с большой частью экосистемы Java. В том числе, JRebel и XRebel основаны на технологии Java-агентов и интегрируются с Java-приложениями, фреймворками, серверами приложений и так далее.
С помощью Java-агента можно инструментировать Java-код так, чтобы добавить нужную тебе дополнительную функциональность. Чтобы протестировать, как приложение ведет себя после применения патча, необходимо запустить его через преднастроенный Java-агент. Как только приложение запустилось и заработало, для воспроизведения желаемого поведения можно послать ему HTTP-запрос.
Чтобы запускать такие тесты на больших масштабах, требуется иметь автоматизированную систему, которая сможет запускать и останавливать среду исполнения, включая сервер приложений или любые другие внешние зависимости, от которых зависит приложение. Должна иметься возможность запускать примерно одни и те же вещи и в среде непрерывной интеграции, и на компьютере разработчика.
В результате получается куча тестов, они работают совсем не быстро, и поэтому мы точно захотим запускать их параллельно. Это автоматом означает, что тесты должны быть изолированы, чтобы не случилось конфликтов по ресурсам. Например, если мы запускаем на одном хосте несколько экземпляров Tomcat, хотелось бы избежать конфликтов использования портов.
В таком интеграционном тестировании нам помогает небольшая красивая библиотека TestContainers. Она не просто подошла по озвученным выше требованиям — после её внедрения мы получили внушительный рост производительности.
TestContainers
Официальная документация TestContainers говорит следующее:
«TestContainers — это Java-библиотека, которая поддерживает тесты JUnit и предоставляет легкие, временные экземпляры основных баз данных, веб-браузеров для Selenium или чего угодно еще, что можно запускать в Docker-контейнере».
TestContainers предоставляет API для автоматизации настройки окружения. Оно запускает нужные Docker-контейнеры ровно на время работы наших тестов и гасит их сразу же, как тесты завершатся. Дальше мы посмотрим на несколько демок, основанных на официальных примерах, лежащих в их репозитории на GitHub.
GenericContainer
При использовании TestContainers, очень часто используется класс
GenericContainer
:public class RedisBackedCacheTest {
@Rule
public GenericContainer redis = new GenericContainer("redis:3.0.6")
.withExposedPorts(6379);
Его конструктор принимает в качестве параметра строку, в которой указывается Docker-образ, который мы в дальнейшем будем использовать. В ходе запуска, TestContainers автоматически загружает соответствующий образ (если он не был загружен ранее).
Важное замечание: в методе
withExposedPorts(6379)
, 6379 — это порт, на котором будет висеть контейнер. Далее мы сможем найти соответствующий ему связанный порт с помощью вызова на экземпляре контейнера метода getMappedPort(6379)
. Объединяя это с getContainerIpAddress()
, можем получить полный URL сервиса, запущенного в контейнере:String redisUrl = redis.getContainerIpAddres() + “:” + redis.getMappedPort(6379);
Можно заметить, что поле из этого примера отмечено аннотацией
@Rule
. Аннотация @Rule из JUnit определяет, что мы будем получать новый экземпляр GenericContainer
в каждом тестовом методе этого класса. Если же мы захотели бы переиспользовать экземпляр контейнера, для этого существует аннотация @ClassRule
.Контейнеры под задачу
Наследники
GenericContainer
— это специализированные под задачу контейнеры. Для тестирования уровня доступа к данным, из коробки имеются контейнеризованные образы MySQL, PostgreSQL и Oracle.PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:9.6.2")
.withUsername(POSTGRES_USERNAME)
.withPassword(POSTGRES_PASSWORD);
Всего лишь этой одной строчкой можно получить экземпляр контейнера, который останется с нами на протяжении теста. На машину, где будут запускаться тесты, не нужно вручную устанавливать базу данных. Это дает особенно большой выигрыш, если хочется провести тест на нескольких версиях одной и той же базы данных.
Свои собственные контейнеры
Наследуясь от
GenericContainer
, возможно делать новые типы контейнеров. Это довольно удобно, если хочется инкапсулировать соответствующие сервисы и логику. Например, можно использовать MockServer, чтобы замокать зависимости распределенной системы, в которой приложения общаются друг с другом по HTTP:public class MockServerContainer extends BaseContainer<MockServerContainer> {
MockServerClient client;
public MockServerContainer() {
super("jamesdbloom/mockserver:latest");
withCommand("/opt/mockserver/run_mockserver.sh -logLevel INFO -serverPort 80");
addExposedPorts(80);
}
@Override
protected void containerIsStarted(InspectContainerResponse containerInfo) {
client = new MockServerClient(getContainerIpAddress(), getMappedPort(80));
}
}
В этом примере, сразу же после инициализации контейнера, используется колбэк
containerIsStarted(...)
, который инициализирует экземпляр MockServerClient
. Таким образом, мы спрятали все детали реализации, специфичные для контейнера, внутри своего собственного типа контейнера. Благодаря этому мы получили более чистый код клиента и более аккуратный API для тестирования.
Дальше мы увидим, что вручную определенные контейнеры помогают в структуризации окружения для тестирования Java-агентов.
Тестирование Java-агента с помощью TestContainers
Для демонстрации идеи воспользуемся примером, любезно предоставленным Сергеем @bsideup Егоровым, сомантейнером проекта TestContainers.
Демонстрационное приложение
Давайте начнем с тестового приложения. Нам понадобится веб-приложение, отвечающее на HTTP GET-запросы. Жирных фреймворков не требуется — поэтому почему бы не взять SparkJava? Чтобы добавить веселья, сразу начнем кодить на Groovy! Вот это приложение мы будем тестировать:
@Grab("com.sparkjava:spark-core:2.1")
import static spark.Spark.*
get("/hello/") { req, res -> "Hello!" }
Это простой скрипт на Groovy, использующий Grape для загрузки зависимости на SparkJava, и определяющий один HTTP-эндпоинт, отвечающий сообщением “Hello!”.
Java-агент
Агент, который мы собрались проверять, патчит сервер Jetty и добавляет ему дополнительный заголовок в HTTP-ответ.
public class Agent {
public static void premain(String args, Instrumentation instrumentation) {
instrumentation.addTransformer(
(loader, className, clazz, domain, buffer) -> {
if ("spark/webserver/JettyHandler".equals(className)) {
try {
ClassPool cp = new ClassPool();
cp.appendClassPath(new LoaderClassPath(loader));
CtClass ct = cp.makeClass(new ByteArrayInputStream(buffer));
CtMethod ctMethod = ct.getDeclaredMethod("doHandle");
ctMethod.insertBefore("{ $4.setHeader(\"X-My-Super-Header\", \"42\"); }");
return ct.toBytecode();
} catch (Throwable e) {
e.printStackTrace();
}
}
return buffer;
});
}
}
В этом примере Javassist используется для патчинга метода
JettyHandler.doHandle
, в который добавляется дополнительная команда, устанавливающая заголовок X-My-Super-Header
.
Конечно, чтобы стать Java-агентом, нужно правильно собраться в пакет и добавить соответствующие аттрибуты в файл
MANIFEST.MF
. Всё это за нас делает сборочный скрипт, чтобы не загромождать статью, он выложен на GitHub, смотрите содержимое файла build.grade.Собственно, тест!
Тест будет довольно простым: нужно сделать запрос к нашему приложению и проверить ответ на наличие особого заголовка, который Java-агент, теоретически, должен бы туда добавить. Если заголовок найден и значение заголовка совпадает с ожидаемым значением — тест успешно пройден. Взглянем на код:
@Test
public void testIt() throws Exception {
// Using Feign client to execute the request
Response response = app.getClient().getHello();
assertThat(response.headers().get("X-My-Super-Header"))
.isNotNull()
.hasSize(1)
.containsExactly("42");
}
Можно запустить его прямо из IDE, или из командной строки, или даже в среде непрерывной интеграции. TestContainers помогают нам запустить приложение так, что агент оказывается в изолированном окружении, в Docker-контейнере.
Чтобы запустить приложение, нужен Docker-образ с поддержкой Groovy. Чтобы сделать себе удобно, мы завели Docker-образ zeroturnaround/groovy, он лежит на Docker Hub. Вот как его можно использовать, наследуясь от
GenericContainer
:public class GroovyTestApp<SELF extends GroovyTestApp<SELF>>
extends GenericContainer<SELF> {
public GroovyTestApp(String script) {
super("zeroturnaround/groovy:2.4.5");
withClasspathResourceMapping("agent.jar", "/agent.jar", BindMode.READ_ONLY);
withClasspathResourceMapping(script, "/app/app.groovy", BindMode.READ_ONLY);
withEnv("JAVA_OPTS", "-javaagent:/agent.jar");
withCommand("/opt/groovy/bin/groovy /app/app.groovy");
withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(script)));
}
public String getURL() {
return "http://" + getContainerIpAddress() + ":"
+ getMappedPort(getExposedPorts().get(0));
}
}
Посмотрите, как API предоставляет нам методы для получения IP-адреса контейнера, а также связанного порта (который в реальности рандомизован). В смысле, порт будет разный каждый раз, когда запускается тест. Поэтому, если запустить все тесты одновременно, не будет конфликтов между портами, и тесты не посыпятся.
Теперь у нас имеется специальный класс
GroovyTestApp
для простого запуска скриптов на Groovy, в нашем случае — для тестирования демонстрационного приложения:GroovyTestApp app = new GroovyTestApp(“app.groovy”)
.withExposedPorts(4567); //the default port for SparkJava
.setWaitStrategy(new HttpWaitStrategy().forPath("/hello/"));
Запускаем тесты, смотрим на выхлоп:
$ ./gradlew test
16:42:51.462 [I] d.DockerClientProviderStrategy - Accessing unix domain socket via TCP proxy (/var/run/docker.sock via localhost:50652)
… … …
16:43:01.497 [I] app.groovy - STDERR: [Thread-1] INFO spark.webserver.SparkServer - == Spark has ignited ...
16:43:01.498 [I] app.groovy - STDERR: [Thread-1] INFO spark.webserver.SparkServer - >> Listening on 0.0.0.0:4567
16:43:01.511 [I] app.groovy - STDERR: [Thread-1] INFO org.eclipse.jetty.server.Server - jetty-9.0.2.v20130417
16:43:01.825 [I] app.groovy - STDERR: [Thread-1] INFO org.eclipse.jetty.server.ServerConnector - Started ServerConnector@72f63426{HTTP/1.1}{0.0.0.0:4567}
16:43:02.199 [I] ?.4.5] - Container zeroturnaround/groovy:2.4.5 started
AgentTest > testIt STANDARD_OUT
Got response:
HTTP/1.1 200 OK
content-length: 6
content-type: text/html; charset=UTF-8
server: Jetty(9.0.2.v20130417)
x-my-super-header: 42
Hello!
BUILD SUCCESSFUL
Total time: 36.014 secs
Тест этот не очень быстр. Какое-то время уходит на скачивание Grapes — но только самый первый раз. Тем не менее, это полноценный интеграционный тест, который запускает Docker-контейнер, приложение с использованием HTTP-стека, и делает HTTP-запросы. Кроме этого, приложение запускается в изоляции, и сделать это действительно просто. И всё это — благодаря TestContainers!
Заключение
«Работает на моем компьютере» — популярное оправдание, но оно больше не должно быть оправданием вообще. По мере того, как технология контейнеризации становится доступна всё большему количеству разработчиков, появляется возможность делать всё более детерминированные тесты.
TestContainers уменьшают количество безумия в интеграционных тестах приложений на Java. Эту библиотеку очень просто интегрировать в существующие тесты. Больше не нужно вручную управлять внешними зависимостями, и это — огромная победа, особенно в среде непрерывной интеграции.
Если вам понравилось то, что вы сейчас прочитали, очень советуем посмотреть на запись с конференции GeekOut Java, где Richard North, изначальный автор проекта, дает вводную информацию о TestContainers, включая планы по развитию. Или хотя бы посмотреть на слайды этой презентации.
Источник: https://habrahabr.ru/company/jugru/blog/343298/?utm_source=habrahabr&utm_medium=rss&utm_campaign=interesting
Смотри также:
- Зачем нужна Java. http://fetisovvs.blogspot.com/2014/07/java.html
- JAVA 9. Что нового? http://fetisovvs.blogspot.com/2017/10/java-9-java.html
- Концепции объектно-ориентированного программирования — ООП в Java. http://fetisovvs.blogspot.com/2017/01/java-java.html
- Java-ресурсы, на которые есть смысл подписаться. http://fetisovvs.blogspot.com/2016/09/java-java.html
- Подборка популярных ошибок начинающих Java программистов. http://fetisovvs.blogspot.com/2016/10/java-java_29.html
- Двести пятьдесят русскоязычных обучающих видео докладов и лекций о Java. http://fetisovvs.blogspot.com/2015/12/java-5-java-java.html
- Абстрактные классы и методы. http://fetisovvs.blogspot.com/2017/02/java.html
- Полное руководство по Java Reflection API. Рефлексия на примерах. http://fetisovvs.blogspot.com/2017/02/java-reflection-api-java.html
- Микросервисы для Java программистов. Практическое введение во фреймворки и контейнеры. http://fetisovvs.blogspot.com/2017/10/java-java.html
- Микросервисы для Java программистов. Практическое введение во фреймворки и контейнеры. (Часть 3). http://fetisovvs.blogspot.com/2017/10/java-3-java.html
- ТОП-3 способа конвертировать массив в ArrayList. Пример на Java. http://fetisovvs.blogspot.com/2016/09/3-arraylist-java-java.html
- Integer и int. http://fetisovvs.blogspot.com/2016/07/integer-int-java.html
- Ввод–вывод в Java. http://fetisovvs.blogspot.com/2016/05/java-java_28.html
- Enum-Всемогущий. http://fetisovvs.blogspot.com/2017/02/enum-java.html
- Массивы в Java. Создание и обработка. http://fetisovvs.blogspot.com/2017/10/java-java_18.html
- Популярные методы для работы с Java массивами. http://fetisovvs.blogspot.com/2016/09/java-java_29.html
- В чем разница между Set и Set. Пример использования Set. http://fetisovvs.blogspot.com/2016/09/set-set-set-java.html
- Пример использования метода replace в Java. Как заменить символ в строке? http://fetisovvs.blogspot.com/2017/01/replace-java-java.html
- Класс Scanner в Java — описание и пример использования. http://fetisovvs.blogspot.com/2017/01/scanner-java-java.html
- Пример использования метода trim в Java: как удалить пробелы в начале и конце строки? http://fetisovvs.blogspot.com/2017/01/trim-java-java.html
- Spark — Потрясающий веб-микрофреймворк для Java. http://fetisovvs.blogspot.com/2017/10/spark-java-java.html
- Чтение и запись CSV файла с помощью SuperCSV. http://fetisovvs.blogspot.com/2017/01/csv-supercsv-java-java.html
- Конструкция try/catch/finally (исключения). http://fetisovvs.blogspot.com/2017/01/trycatchfinally-java.html
- 1000+ часов видео по Java на русском. http://fetisovvs.blogspot.nl/2017/06/1000-java-java.html
- Шпаргалка Java программиста 7.1 Типовые задачи: Оптимальный путь преобразования InputStream в строку. http://fetisovvs.blogspot.com/2016/04/java-71-inputstream-java.html
- Шпаргалка Java программиста 8. Библиотеки для работы с Json (Gson, Fastjson, LoganSquare, Jackson, JsonPath и другие). http://fetisovvs.blogspot.com/2016/04/java-8-json-gson-fastjson-logansquare.html
- Реализация ООП-наследования в классах, работающих с SQL и MS Entity Framework. http://fetisovvs.blogspot.com/2017/02/sql-ms-entity-framework.html
- Как установить соединение с СУБД MySQL в IntelliJ IDEA в редакции Community. http://fetisovvs.blogspot.com/2016/04/mysql-intellij-idea-community-java.html
- Работа с Bluetooth LE из Java-приложений. http://fetisovvs.blogspot.com/2016/07/bluetooth-le-java-java.html
- Как с помощью maven работать с библиотеками, которых в maven нет. http://fetisovvs.blogspot.com/2017/03/maven-maven-java.html
- Диагностика утечек памяти в Java. http://fetisovvs.blogspot.com/2017/03/java-java_18.html
- Программируем… выход из лабиринта. http://fetisovvs.blogspot.com/2015/10/java.html
- Основы работы с IntelliJ IDEA. Интерфейс программы. http://fetisovvs.blogspot.com/2016/09/intellij-idea-java.html
Комментариев нет:
Отправить комментарий