Testcontainers: тестирование с реальными зависимостями
Программное обеспечение развивается с течением времени, и автоматизированное тестирование является необходимым условием для непрерывной интеграции и непрерывной доставки. Разработчики пишут различные типы тестов, такие как модульные тесты, интеграционные тесты, тесты производительности и E2E-тесты для измерения различных аспектов программного обеспечения.
Обычно модульное тестирование выполняется для проверки только бизнес-логики, и в зависимости от тестируемой части системы для внешних зависимостей, как правило, используются макеты или заглушки.
Но одни только модульные тесты не обеспечивают большой уверенности, потому что фактическая сквозная функциональность зависит от интеграции различных внешних сервисов. Поэтому интеграционные тесты используются для проверки общего поведения системы с помощью реальных зависимостей.
Традиционно интеграционное тестирование представляет собой сложный процесс, который может включать следующие этапы:
Установка и настройка необходимых зависимых служб, таких как базы данных, брокеры сообщений и т. д.
Настройка веб-сервера или сервера приложений
Создание и развертывание артефакта (jar, war, нативный исполняемый файл и т. д.) на сервере.
Наконец, запуск интеграционных тестов
Однако, используя Testcontainers, вы можете получить как легкость и простоту модульных тестов, так и надежность интеграционных тестов, работающих с реальными зависимостями.
1. Почему важно тестирование с реальными зависимостями
Тесты должны позволять разработчикам проверять поведение приложения с помощью быстрых циклов обратной связи во время фактической разработки.
Тестирование с помощью макетов или сервисов в памяти не только создает ложное впечатление о том, что система работает нормально, но также может значительно задержать цикл обратной связи. Тесты с использованием реальных зависимостей проверяют реальный код и дают больше уверенности.
Рассмотрим распространенный сценарий использования баз данных в памяти, таких как H2, для тестирования при использовании Postgres или SQL Server в производственной среде. Есть несколько причин, почему это плохая практика.
Проблемы совместимости
Любое нетривиальное приложение будет использовать некоторые специфичные для базы данных функции, которые могут не поддерживаться базами данных в памяти. Например, распространенный способ применения пагинации является использование LIMIT и OFFSET.
SELECT id, name FROM employee ORDER BY name LIMIT 25 OFFSET 50
Представьте, что вы используете базу данных H2 для тестирования и MS SQL Server в качестве производственной базы данных. Когда вы тестируете с помощью H2, тесты пройдут, создавая ошибочное впечатление, что ваш код работает нормально, но в производственной среде они провалятся, потому что MS SQL Server не поддерживает синтаксис LIMIT … OFFSET.
Базы данных в памяти могут не поддерживать все функции вашей производственной базы данных
Ваше приложение может использовать специфические для производителя базы данных расширенные возможности, такие как функции преобразования XML/JSON, WINDOW-функции и общие табличные выражения (CTE), которые могут не полностью поддерживаться базами данных в памяти. В таких случаях тестирование с использованием баз данных в памяти становится невозможным.
Очень часто они перерастают в еще большую проблему, когда вы имитируете сервисы в своем собственном коде. Хотя макеты могут помочь в тестировании сценариев, в которых вы можете успешно извлечь определение макета для использования в качестве контракта для сервисов, очень часто такая проверка совместимости только добавляет сложности в настройку теста.
А обычное использование макетов не позволяет ни надежно проверить, что поведение вашей системы будет работать в производственной среде, ни дает уверенности в том, что набор тестов отловит проблемы при возникновении несовместимости с вашим кодом и сторонними интеграциями.
Поэтому настоятельно рекомендуется писать тесты с использованием реальных зависимостей как можно чаще и использовать имитаторы только в редких случаях.
2. Тестирование с использованием реальных зависимостей с помощью Testcontainers
Testcontainers — это библиотека тестирования, которая позволяет писать тесты с использованием реальных зависимостей с помощью одноразовых контейнеров Docker. Она предоставляет программируемый API для создания необходимых зависимых сервисов в виде контейнеров Docker, чтобы вы могли писать тесты, используя реальные сервисы вместо макетов. Таким образом, независимо от того, пишете ли вы модульные тесты, тесты API или сквозные тесты, вы можете писать тесты с использованием реальных зависимостей с помощью одной и той же модели программирования.
Библиотеки Testcontainers доступны для следующих языков и хорошо интегрируются с большинством фреймворков и библиотек тестирования:
Java
Go
Node.js
.NET
Python
Rust
3. Исследование примера
Давайте посмотрим, как Testcontainers можно использовать для тестирования различных частей приложения, и все они выглядят как «модульные тесты с реальными зависимостями».
В этой статье мы будем использовать пример кода из приложения SpringBoot, реализующего типичный API-сервис, который используется через веб-приложение и использует Postgres для хранения данных.
Но поскольку Testcontainers предоставляет вам идиоматический API для вашего любимого языка, аналогичная настройка может быть выполнена во всех них.
Поэтому рассматривайте эти примеры как иллюстрации, чтобы получить представление о том, что возможно. И если вы работаете в экосистеме Java, то вы узнаете тесты, которые вы писали в прошлом, или получите стимул к тому, как это можно сделать.
3.1. Тестирование репозиториев данных
Допустим, у нас есть следующий репозиторий Spring Data JPA с одним пользовательским методом.
public interface TodoRepository extends PagingAndSortingRepository<Todo, String> {
@Query("select t from Todo t where t.completed is false")
Iterable<Todo> getPendingTodos();
}
Как мы уже говорили выше, использование базы данных в памяти для тестирования, в то время как для производства используется другой тип базы данных, совсем не рекомендуется и может вызвать множество проблем. Функции или синтаксис запроса, поддерживаемые вашим типом производственной базы данных, могут не поддерживаться базой данных в памяти.
Например, следующий запрос, который вы можете использовать в сценариях миграции данных, будет отлично работать в Postgresql, но сломается в случае с H2.
INSERT INTO todos (id, title)
VALUES ('1', 'Learn Modern Integration Testing with Testcontainers')
ON CONFLICT do nothing;
Поэтому всегда рекомендуется проводить тестирование с тем же типом базы данных, который используется в производственной среде.
Мы можем написать модульные тесты для TodoRepository, используя аннотацию @DataJpaTest для срезовых тестов SpringBoot, создав контейнер Postgres с помощью Testcontainers следующим образом:
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class TodoRepositoryTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:14-alpine");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
TodoRepository repository;
@BeforeEach
void setUp() {
repository.deleteAll();
repository.save(new Todo(null, "Todo Item 1", true, 1));
repository.save(new Todo(null, "Todo Item 2", false, 2));
repository.save(new Todo(null, "Todo Item 3", false, 3));
}
@Test
void shouldGetPendingTodos() {
assertThat(repository.getPendingTodos()).hasSize(2);
}
}
Зависимость базы данных Postgres обеспечивается с помощью Testcontainers JUnit5 Extension, и тест взаимодействует с реальной базой данных Postgres. Для получения дополнительной информации об использовании управления жизненным циклом контейнеров смотрите раздел Интеграция Testcontainers и JUnit.
Тестирование с использованием базы данных того же типа, который используется для производственной среды, вместо использования базы данных в памяти, позволяет полностью избежать проблем совместимости баз данных и повышает доверие к нашим тестам.
Для тестирования баз данных Testcontainers предоставляют специальную поддержку JDBC URL, которая облегчает работу с базами данных SQL.
3.2. Тестирование конечных точек REST API
Мы можем протестировать конечные точки API, загружая приложение вместе с необходимыми зависимостями, такими как база данных, предоставленная через Testcontainers. Модель программирования для тестирования конечных точек REST API такая же, как и для модульного тестирования репозитория.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
public class TodoControllerTests {
@LocalServerPort
private Integer port;
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:14-alpine");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
TodoRepository todoRepository;
@BeforeEach
void setUp() {
todoRepository.deleteAll();
RestAssured.baseURI = "http://localhost:" + port;
}
@Test
void shouldGetAllTodos() {
List<Todo> todos = List.of(
new Todo(null, "Todo Item 1", false, 1),
new Todo(null, "Todo Item 2", false, 2)
);
todoRepository.saveAll(todos);
given()
.contentType(ContentType.JSON)
.when()
.get("/todos")
.then()
.statusCode(200)
.body(".", hasSize(2));
}
}
Мы загрузили приложение с помощью аннотации @SpringBootTest и использовали RestAssured для выполнения вызовов API и проверки ответа. Это даст нам больше уверенности в наших тестах, поскольку в них не задействованы макеты, и позволит разработчикам выполнять любой внутренний рефакторинг кода, не нарушая API-контакта.
3.3. Сквозное тестирование с использованием Selenium и Testcontainers
Selenium — популярный инструмент автоматизации браузера для проведения сквозного тестирования. Testcontainers предоставляет модуль Selenium, упрощающий выполнение тестов на основе Selenium в контейнере Docker.
@Testcontainers
public class SeleniumE2ETests {
@Container
static BrowserWebDriverContainer<?> chrome = new BrowserWebDriverContainer<>().withCapabilities(new ChromeOptions());
static RemoteWebDriver driver;
@BeforeAll
static void beforeAll() {
driver = new RemoteWebDriver(chrome.getSeleniumAddress(), new ChromeOptions());
}
@AfterAll
static void afterAll() {
driver.quit();
}
@Test
void testViewHomePage() {
String baseUrl = "https://myapp.com";
driver.get(baseUrl);
assertThat(driver.getTitle()).isEqualTo("App Title");
}
}
Мы можем запускать Selenium-тесты с помощью той же модели программирования, используя WebDriver, предоставленный Testcontainers. Testcontainers даже позволяют легко записывать видео выполнения тестов без необходимости выполнять сложную настройку конфигурации.
Для справки вы можете взглянуть на проект Testcontainers Java SpringBoot QuickStart.
4. Заключение
Мы рассмотрели различные типы тестов, которые разработчики используют в своих приложениях: уровень доступа к данным, тесты API и даже сквозные тесты. Также было рассмотрено использование библиотек Testcontainers для упрощения настройки для запуска этих тестов с реальными зависимостями, такими как реальная версия базы данных, которую вы будете использовать в производственной среде.
Testcontainers доступна на нескольких популярных языках программирования, таких как Java, Go, .NET и Python, и предоставляет вам идиоматический подход к преобразованию ваших тестов с реальными зависимостями в модульные тесты, которые знают и любят разработчики.
Тесты на основе Testcontainers одинаково выполняются как в вашем конвейере CI, так и локально, независимо от того, решите ли вы запустить отдельный тест в своей IDE, класс тестов или даже весь набор тестов из командной строки, что обеспечивает непревзойденную воспроизводимость проблем и опыт разработчика.
Более того, Testcontainers позволяет писать тесты с использованием реальных зависимостей без необходимости использования макетов, что придает больше уверенности вашему набору тестов. Итак, если вы сторонник практичного подхода, ознакомьтесь с Testcontainers Java SpringBoot QuickStart, содержащим все типы тестов, которые мы рассмотрели в этой статье, сразу же доступные для запуска!