Разработчики со своей стороны пытаются минимизировать эти риски и пишут тесты: unit и интеграционные. Написание unit тестов обычно не составляет каких-либо трудностей. С интеграционными тестами, в зависимости от их изощренности, ситуация сложнее.
Когда тесты используют Tomcat или Jetty это не доставляет никаких проблем: эти сервера написаны на java и могут легко встраиваться в тесты. Но, например, мы используем Aerospike и когда мы хотим протестировать взаимодействие с этой базой данных, нас ожидают следующие трудности:
- Aerospike написан не на Java и не может быть легко встроен в наше приложение.
- Хочется чтобы разработчик мог запустить тесты на всех популярных платформах: Windows, OS X или Linux. Aerospike же предоставляет бинарники только для Linux.
- Тесты могут выполняться параллельно, следовательно нам нужно несколько серверов?
- Каждый тест должен получать в распоряжение чистый экземпляр базы данных.
Часто для этих целей используется общее тестовое окружение, например удаленный сервер, на котором уже настроен Aerospike и на котором можно запустить некоторые тесты. Однако, подход обладает определенными недостатками:
- Запуск тестов на удаленном сервере занимает существенно больше времени, чем запуск локальных тестов. Это особенно заметно при работе с медленным интернетом.
- Возникают проблемы с изоляцией, в случае, если несколько разработчиков решили одновременно протестировать приложение.
- Довольно часто политика информационной безопасности компании требует прятать сервера за корпоративным VPN. Перспектива настройки VPN клиента может отвадить все желание работать из дома.
Альтернативный вариант: требовать подготовить локальное окружение для тестов, писать длинные инструкции по установке и настройке Aerospike для трех платформ (Win/Mac/Linux). Но есть и другой вариант — воспользоваться утилитами автоматизации, такими, как Docker.
Docker — это система развертывания и управления приложениями в изолированной среде (контейнерах). Он построен на принципах клиент-серверной архитектуры, docker client есть для всех основных OC, а docker daemon работает только на Linux — системах. Однако это не проблема: с помощью docker-machine можно запустить docker и на Windows/OS X (правда на виртуальном хосте). Итак, чтобы запустить тесты разработчику необходимо иметь на машине настроенный docker client — проверить это можно командой: docker run hello-world. Для Windows и OS X потребуется установить docker-machine.
Embedded aerospike
Для использования Aerospike в интеграционных тестах, мы написали обертку для docker и docker-machine. Она умеет:
- Стартовать / останавливать контейнеры.
- Смонтировать конфигурационный файл aerospike.conf внутрь контейнера.
- Привязать порт контейнера к свободному порту хоста.
- Стартовать / останавливать Docker Machine если тесты запускаются на Windows/OS X.
Для управления контейнерами мы используем docker-java API client - популярный Java API клиент для Docker. Чтобы запустить сервер Aerospike внутри Docker контейнера, настроить проброс портов и смонтировать конфигурационный файл, нужно выполнить команду:
docker run -d -P -p 3000:3000 -v
path/to/aerospike.conf:/etc/aerospike/aerospike.conf --name aerospike aerospike
А вот код, который делает тоже самое, используя Docker Remote Api
ExposedPort tcp3000 = ExposedPort.tcp(3000);
Volume volume = new Volume("/etc/aerospike/aerospike.conf");
Ports portBindings = new Ports();
portBindings.bind(tcp3000, Ports.binding(aerospikePort));
CreateContainerResponse container = dockerClient.createContainerCmd(IMAGE_ID)
.withExposedPorts(tcp3000)
.withPortBindings(portBindings)
.withBinds(new Bind(aerospikeConfPath, volume, AccessMode.ro))
.exec();
dockerClient.startContainerCmd(container.getId())
.exec();
Более детально можно посмотреть в классе AerospikeServer.
Пишем тест
Давайте рассмотрим пример интеграционного теста с использованием embedded-aerospike. Допустим у нас есть класс SimpleAerospikeClient, который умеет хранить и получать сегменты пользователя по идентификатору.
public Set getSegments(Long userId);
public void addSegments(Long userId, Set segments);
Посмотреть полный класс.
Пишем тест, который проверяет корректность реализации данных методов.
Сначала надо настроить и запустить сервер.
@BeforeMethod
public void setUp() throws Exception {
aerospikeServer = AerospikeServer.builder()
.aerospikeConfPath(getClass().getResource("/aerospike.conf").getFile())
.dockerConfig(DockerClientConfig.createDefaultConfigBuilder().build())
.build();
aerospikeServer.start();
}
Проверяем, что данные правильно записываются и читаются из базы данных.
@Test
public void test() {
long userId = ThreadLocalRandom.current().nextLong();
aerospikeClient.addSegments(userId, new HashSet<Integer>() {{
add(150);
add(151);
}});
Set<Integer> segments = aerospikeClient.getSegments(userId);
Assert.assertEquals(segments.size(), 2);
Assert.assertTrue(segments.contains(150));
Assert.assertTrue(segments.contains(151));
}
Не забываем остановить и удалить контейнеры, которые были созданы во время тестов.
@AfterMethod
public void tearDown() throws Exception {
aerospikeServer.stop();
}
Заключение
Такой подход позволяет запускать интеграционные тесты на машине разработчика, а это значит, что во-первых вам не надо тратить ресурсы на поддержание тестовых серверов, а во-вторых вы можете разрабатывать и тестировать приложение без доступа к инфраструктуре. Мы рассмотрели пример работы с Aerospike, но очевидно что таким образом можно тестировать взаимодействие вашей программы с любыми сервисами.
Авторы статьи — kiruxan и zeliboba69
Комментарии (12)
jrip
26.05.2016 19:49>Для использования Aerospike в интеграционных тестах, мы написали обертку для docker и docker-machine. Она умеет:
>Стартовать / останавливать контейнеры.
>Смонтировать конфигурационный файл aerospike.conf внутрь контейнера.
>Привязать порт контейнера к свободному порту хоста.
А почему не использовали для этого Chef?
bsideup
26.05.2016 23:10Как один из мэйнтейнеров проекта не могу не спросить:
А почему не http://github.com/testcontainers/testcontainers-java? :)
kiruxan
27.05.2016 12:44Все просто: хоть статья и написана недавно, но данный подход мы используем с 2015 года когда проект TestContainers видимо только стартовал, ну и плюс все тесты в проекте на TestNG. Но библиотека у вас очень интересная, как минимум смотреть в её сторону имеет смысл!
bsideup
27.05.2016 12:48https://github.com/testcontainers/testcontainers-java/issues/132 :)
На самом деле у нас тоже были тесты на TestNG, мы просто создавали контейнеры из TC и вручную вызывали .start() и .stop().
Как и написал выше, мы тоже начали со своего, а потом просто присоединились к TestContainers. OpenSource FTW! :)
Можете постучаться в разного рода IM-ы (Gitter, Skype ), я помогу смигрировать на TC если надо :)
methodx
Статья отличная, но картинка — тяжелейшая форма депрессии.
hanovruslan
Это из иллюстрации к корнею чуковскому. Для вас это бред, а для нас (постарше) это тест на школоло )) шутка, без обид.
А если серьезно то тк автору у меня вопросы
непонятно зачем так реализовывать модуль к фреймворку тестирования (у вас гольный junit или как?)
Стал ли этот вспомогательный код частью приложения?
Не похоже ли это на антипаттерн тестирование моков?
zeliboba69
hanovruslan, под вспомогательным кодом вы имеете ввиду SimpleAerospikeClient.java? Это, скажем так, упрощенная версия того, что используется в приложении.
Не совсем понял про тестирование моков, ведь идея как раз в том, чтобы написать интеграционный тест. Тест, который, в данном случае, будет проверять взаимодействие приложения с базой данных: как минимум, правильно ли вы настроили клиент для работы с базой, правильно ли выбрали структуру данных для хранения, и т.д.
hanovruslan
zeliboba69
Да, про тестирование моков я немного не в тему сказал. Задам другой вопрос ) не знаю как там в джава, но в php есть варианты проще перехватывать подобные обращения (у вас их к слову нет, вы поднимаете какой-то инстанс нужного сервиса) — SoftMocks или runkit. это инструменты которые перегружают библиотечные (недоступные в исходном виде) модули\функции, чтобы обеспечить как и в вашем случае — интеграционные тесты. Так вот привлечение докера (только для интеграционных тестов?) так как-бы немного сбоку смотрится странным решением.
Может быть следует сделать сборку на docker-compose для нескольких окружений (dev, test, prod) где не потребуется использовать SimpleAerospikeClient а взаимодействие клиентов и их серверами будет обеспечено сетью, которую поднимает докер при запуске группы контейнеров? Это, скорее всего сложнее, но выглядит имхо канонично в рамках использования контейнеров
а еще что у вас с фреймворком тестирования? Это JUnit? А есть ему альтернативы?
olegchir
альтернатива JUnit, например, TestNG. Но зачем ему альтернативы? JUnit просто как автомат калашникова, там с десяток методов с общим смыслом assertTrue(message, desired, actual) и всё. Код JUnit'а простейший, можно допиливать его и допиливать как хочешь. Вроде, там просто нечего заменять, это и так минимальный минимум.
Borz
преимущество TestNG перед JUnit как минимум в более удобных DataProvider. В остальном — всё равно, что TestNG, что JUnit. Ну, разве что только то, что в интернете больше примеров по JUnit, чем по TestNG