Как отказаться от заглушек в автотестах с помощью Testcontainers
Testcontainers — ценный инструмент для Java-разработчиков и автоматизаторов, которые хотят изолировать свои тесты и запускать их в чистой среде. Это могут быть базы данных SQL, NoSQL, брокеры сообщений или любые контейнеризированные службы.
При помощи Testсontainers создаются качественные наборы тестов, которые проверяют реальную интеграцию без заглушек. Таким образом можно воссоздать рабочее окружение в тех областях, где сложно заменить реальный ресурс на mock-сервис. Можно сказать, что Testcontainers — это Java-библиотека, которая позволяет автоматизировать тестирование в контейнерах Docker.
Как работает Testсontainers
Данная технология предоставляет простой интерфейс API для запуска контейнеров Docker в рамках тестового процесса. Это позволяет разработчикам тестировать их приложения, используя реальные базы данных, веб-серверы и другие зависимости, как если бы они запускались в продуктивной среде.
Testсontainers поддерживает различные типы контейнеров и несколько версий операционных систем, что позволяет разработчикам тестировать свои приложения на различных платформах.
Библиотека Testcontainers предлагает API для создания нужных служб в виде контейнеров, так что вы можете написать модульные, API или E2E-тесты, используя реальные службы вместо заглушек.
Также стоит отметить функцию многократного использования контейнеров Reusable Containers. Указанный способ позволяет сохранить состояние контейнеров между тестовыми сессиями, т. е. переиспользовать его.
Преимущества и недостатки Testсontainers
Testcontainers предоставляет легковесные, временные экземпляры общих баз данных, веб-браузеров Selenium или любых других сервисов, которые могут работать в контейнере Docker. Вот некоторые недостатки и преимущества использования этой библиотеки:
➕ Быстрая настройка. Testcontainers легко настраивает контейнеры для тестирования с помощью нескольких строк кода
➕ Изоляция. каждый тест запускается в отдельном контейнере, что обеспечивает чистую среду и гарантирует, что тесты не влияют друг на друга
➕ Универсальность. Testcontainers поддерживает широкий спектр приложений и технологий, включая базы данных, брокеры сообщений и веб-браузеры
➕ Совместимость.Testcontainers работает на нескольких платформах, включая Windows, MacOS и Linux |
➖ Оверхед производительности. запуск тестов в контейнерах может привести к некоторому оверхеду производительности, особенно при запуске многих тестов параллельно
➖ Требование Docker. для работы Testcontainers требуется установленный Docker на системе, где запускаются тесты, что может стать препятствием для некоторых пользователей
➖ Сложность. Testcontainers может добавить сложности в вашу систему тестирования, особенно если вы не знакомы с Docker и контейнерами |
Чтобы использовать Testcontainers в связке с Test IT, можно пойти следующим путем:
-
Настройте Testcontainers, чтобы он автоматически запускал необходимые контейнеры Docker при запуске тестов. Это можно сделать с помощью TestContainers JUnit Rule или TestNG Listener.
-
Напишите тесты, использующие контейнеры Docker, и загрузите их в вашу систему управления тестированием.
-
Настройте Test IT для запуска тестов, которые используют Testcontainers.
-
Готово. Организуйте тесты и управляйте ими в Test IT.
Практическая часть
Тестовый проект GitHub находится тут.
План работ
- Подготовка окружения
- Обзор тестового приложения
- Добавление общего теста
- Написание первого интеграционного теста с Testcontainers
- Написание теста с RestAssured с проверкой активатора Spring Boot
- Добавление test containers Redis
- Написание API-тестов с проверкой логики приложения
Описание шагов
1. Подготовка окружения
Инструкция: Step 1: Getting Started
2. Обзор тестового приложения
Оно представляет простой микросервис на основе Spring Boot. Приложение будет включать Redis, Kafka, API. Более подробно можно прочитать в Step 2: Exploring the app
3. Добавление общего теста
Создайте тестовый класс в проекте
src/test/java/com/example/demo/support/AbstractIntegrationTest
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) public class AbstractIntegrationTest { @Test public void contextLoads() { } }
Добавьте файл schema.sql со скриптом в src/test/resources/
CREATE TABLE IF NOT EXISTS talks( id VARCHAR(64) NOT NULL, title VARCHAR(255) NOT NULL, PRIMARY KEY (id) ); INSERT INTO talks (id, title) VALUES ('testcontainers-integration-testing', 'Modern Integration Testing with Testcontainers') ON CONFLICT do nothing; INSERT INTO talks (id, title) VALUES ('flight-of-the-flux', 'A look at Reactor execution model') ON CONFLICT do nothing;
При запуске автотеста будет ошибка, которая означает что H2 не поддерживает PostgreSQL SQL syntax:
... Caused by: org.h2.jdbc.JdbcSQLException: Syntax error in SQL statement "INSERT INTO TALKS (ID, TITLE) VALUES ('testcontainers-integration-testing', 'Modern Integration Testing with Testcontainers') ON[*] CONFLICT DO NOTHING"; ...
Более подробно можно прочитать в Step 3: Adding some tests
4. Написание первой интеграции Testсontainers
Подключите БД postgres для интеграционного теста. Для этого обновите аннотацию @SpringBootTest с указанием spring.datasource.url
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "spring.datasource.url=jdbc:tc:postgresql:14-alpine://testcontainers/workshop" })
Запустите тест contextLoads.
Ожидаемый результат выполнения — успешный. Пример успешного логирования:
2023-02-14 22:59:24.451 INFO 6456 --- [ main] org.testcontainers.DockerClientFactory : Docker host IP address is localhost 2023-02-14 22:59:24.500 INFO 6456 --- [ main] org.testcontainers.DockerClientFactory : Connected to docker: Server Version: 20.10.6 API Version: 1.41 Operating System: Docker Desktop Total Memory: 9935 MB
Более подробно можно прочитать в Step 4: Your first Testcontainers integration
5. Написание автотеста RestAssured для проверки Actuator в Spring Boot приложении
Для этого обновите AbstractIntegrationTest. Вставьте в него следующий код:
protected RequestSpecification requestSpecification; @LocalServerPort protected int localServerPort; @BeforeEach public void setUpAbstractIntegrationTest() { RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); requestSpecification = new RequestSpecBuilder() .setPort(localServerPort) .addHeader( HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE ) .build(); }
Напишите тест RestAssured для проверки endpoint actuator в нашем приложении в классе AbstractIntegrationTest:
@Test public void healthy() { given(requestSpecification) .when() .get("/actuator/health") .then() .statusCode(200) .log().ifValidationFails(LogDetail.ALL); }
После запуска ожидаемым результатом является ошибка:
java.lang.AssertionError: 1 expectation failed. Expected status code <200> but was <503>.
Причиной этой ошибки является то, что не был найден Redis и не был включен параметр autoconfigurable.
Более подробно можно прочитать в Step 5: Hello, r u 200 OK?
6. Подключение Redis
Подключите Redis в проекте с конфигурацией:
static final GenericContainer redis = new GenericContainer("redis:6-alpine") .withExposedPorts(6379); @DynamicPropertySource public static void configureRedis(DynamicPropertyRegistry registry) { redis.start(); registry.add("spring.redis.host", redis::getHost); registry.add("spring.redis.port", redis::getFirstMappedPort); }
Запустите повторно RestAssured автотест healthy. Ожидаемый результат автотеста — успешный.
Более подробно можно прочитать в Step 6: Adding Redis
7. Написание API автотеста для проверки бизнес логики
Для этого создайте класс в RatingsControllerTest в com.example.demo.api
public class RatingsControllerTest extends AbstractIntegrationTest { @Test public void testRatings() { String talkId = "testcontainers-integration-testing"; given(requestSpecification) .body(new Rating(talkId, 5)) .when() .post("/ratings") .then() .statusCode(202); await().untilAsserted(() -> { given(requestSpecification) .queryParam("talkId", talkId) .when() .get("/ratings") .then() .body("5", is(1)); }); for (int i = 1; i <= 5; i++) { given(requestSpecification) .body(new Rating(talkId, i)) .when() .post("/ratings"); } await().untilAsserted(() -> { given(requestSpecification) .queryParam("talkId", talkId) .when() .get("/ratings") .then() .body("1", is(1)) .body("2", is(1)) .body("3", is(1)) .body("4", is(1)) .body("5", is(2)); }); } @Test public void testUnknownTalk() { String talkId = "cdi-the-great-parts"; given(requestSpecification) .body(new Rating(talkId, 5)) .when() .post("/ratings") .then() .statusCode(404); } }
После запуска созданных API автотестов результат их выполнения будет отрицательный, т. е. тесты не пройдены. Причина их падения в том, что для данных тестов требуется подключить KafkaContainer. В классе AbstractIntegrationTest настройте параметры:
static final KafkaContainer kafka = new KafkaContainer( DockerImageName.parse("confluentinc/cp-kafka:5.4.3")); @DynamicPropertySource public static void configureRedis(DynamicPropertyRegistry registry) { Stream.of(redis, kafka, postgres).parallel().forEach(GenericContainer::start); registry.add("spring.redis.host", redis::getHost); registry.add("spring.redis.port", redis::getFirstMappedPort); registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers); }
Более подробно можно прочитать в Step 7: Test the API
На данном этапе воркшоп подошел к завершению. Мы написали интеграционные тесты с Redis, Kafka, SpringBoot.
Можно ли обойтись без Testcontainers?
Если вы не используете внешние ресурсы в приложении, то вам, вероятно, не понадобится Testcontainers в тестировании. Вместо этого можно использовать фиктивные реализации этих зависимостей или просто создать их в коде тестов.
Если же ваше приложение зависит от внешних ресурсов, то есть несколько вариантов, как вы можете обойтись без Testcontainers:
-
Использовать тестовые окружения. Вы можете создать тестовые окружения, которые содержат все необходимые зависимости, и запускать тесты внутри этих окружений. Это эффективное решение, но оно может потребовать большого количества времени и ресурсов на настройку и поддержку таких окружений
-
Использовать встроенные базы данных и брокеров сообщений. Некоторые базы данных и брокеры сообщений имеют встроенные версии, которые могут быть использованы в тестировании. Это более простое решение, чем создание тестовых окружений, но оно может иметь ограничения в функциональности и производительности
-
Использовать другие инструменты. Существуют и другие инструменты для управления внешними зависимостями в тестировании, такие как TestContainers-Kotlin или Embedded Redis. Вы можете использовать эти инструменты, если они лучше соответствуют вашим потребностям
Таким образом, Testcontainers может быть полезным, но не обязательным в тестировании, и решение о его использовании зависит от вашей специфической ситуации.