Как нам помог Docker в написании тестов
Чтобы показывать вам рекламу — мы в GetIntent должны быть уверены в устойчивой и безотказной работе нашей рекламной платформы. Надежность системы складывается из многих компонентов: тип используемого железа, системная/сетевая конфигурация и архитектура приложения. Внесение изменений в достаточно сложные, распределенные приложения всегда несет в себе риск.
Разработчики со своей стороны пытаются минимизировать эти риски и пишут тесты: 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() {{
add(150);
add(151);
}});
Set 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, но очевидно что таким образом можно тестировать взаимодействие вашей программы с любыми сервисами.