Автор оригинала: Mona Mohamadinia.
1. Обзор
Современные приложения не живут изолированно: нам обычно нужно подключаться к различным внешним компонентам, таким как PostgreSQL, Apache Kafka, Cassandra, Redis и другим внешним API.
В этом уроке мы увидим, как SpringFramework 5.2.5 облегчает тестирование таких приложений с введением динамических свойств .
Во-первых, мы начнем с определения проблемы и посмотрим, как мы раньше решали эту проблему далеко не идеальным способом. Затем мы представим аннотацию @DynamicPropertySource и посмотрим, как она предлагает лучшее решение той же проблемы. В конце концов, мы также рассмотрим другое решение из тестовых фреймворков, которое может быть лучше по сравнению с чистыми решениями Spring.
2. Проблема: Динамические свойства
Предположим, мы разрабатываем типичное приложение, которое использует PostgreSQL в качестве базы данных. Мы начнем с простой сущности JPA:
@Entity @Table(name = "articles") public class Article { @Id @GeneratedValue(strategy = IDENTITY) private Long id; private String title; private String content; // getters and setters }
Чтобы убедиться, что эта сущность работает должным образом, мы должны написать для нее тест для проверки взаимодействия с базой данных. Поскольку этот тест должен работать с реальной базой данных, мы должны заранее настроить экземпляр PostgreSQL.
Существуют различные подходы к настройке таких инфраструктурных инструментов во время выполнения тестов . На самом деле существует три основные категории таких решений:
- Настройте отдельный сервер базы данных где-нибудь только для тестов
- Используйте некоторые легкие, специфичные для тестов альтернативы или подделки, такие как H2
- Пусть тест сам управляет жизненным циклом базы данных
Поскольку мы не должны различать наши тестовые и производственные среды, существуют лучшие альтернативы по сравнению с использованием двойников тестов , таких как H2 . Третий вариант, в дополнение к работе с реальной базой данных, предлагает лучшую изоляцию для тестов . Более того, с такими технологиями , как Docker и Тестовые контейнеры , легко реализовать третий вариант.
Вот как будет выглядеть наш рабочий процесс тестирования, если мы будем использовать такие технологии, как тестовые контейнеры:
- Настройте компонент, такой как PostgreSQL, перед всеми тестами. Обычно эти компоненты прослушивают случайные порты.
- Проведите тесты.
- Снесите компонент.
Если наш контейнер PostgreSQL будет каждый раз прослушивать случайный порт, то мы должны каким-то образом установить и изменить свойство конфигурации spring.datasource.url динамически . В принципе, каждый тест должен иметь свою собственную версию этого свойства конфигурации.
Когда конфигурации статичны, мы можем легко управлять ими с помощью средства управления конфигурациями Spring Boot. Однако, когда мы сталкиваемся с динамическими конфигурациями, та же самая задача может быть сложной.
Теперь, когда мы знаем проблему, давайте рассмотрим ее традиционное решение.
3. Традиционное Решение
Первый подход к реализации динамических свойств заключается в использовании пользовательского ApplicationContextInitializer . В принципе, мы сначала настраиваем нашу инфраструктуру и используем информацию с первого шага для настройки ApplicationContext :
@SpringBootTest @Testcontainers @ContextConfiguration(initializers = ArticleTraditionalLiveTest.EnvInitializer.class) class ArticleTraditionalLiveTest { @Container static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:11") .withDatabaseName("prop") .withUsername("postgres") .withPassword("pass") .withExposedPorts(5432); static class EnvInitializer implements ApplicationContextInitializer{ @Override public void initialize(ConfigurableApplicationContext applicationContext) { TestPropertyValues.of( String.format("spring.datasource.url=jdbc:postgresql://localhost:%d/prop", postgres.getFirstMappedPort()), "spring.datasource.username=postgres", "spring.datasource.password=pass" ).applyTo(applicationContext); } } // omitted }
Давайте пройдемся по этой несколько сложной настройке. JUnit создаст и запустит контейнер раньше, чем что-либо еще. После того, как контейнер будет готов, расширение Spring вызовет инициализатор для применения динамической конфигурации к среде Spring |/. Очевидно, что этот подход немного многословен и сложен.
Только после этих шагов мы можем написать наш тест:
@Autowired private ArticleRepository articleRepository; @Test void givenAnArticle_whenPersisted_thenShouldBeAbleToReadIt() { Article article = new Article(); article.setTitle("A Guide to @DynamicPropertySource in Spring"); article.setContent("Today's applications..."); articleRepository.save(article); Article persisted = articleRepository.findAll().get(0); assertThat(persisted.getId()).isNotNull(); assertThat(persisted.getTitle()).isEqualTo("A Guide to @DynamicPropertySource in Spring"); assertThat(persisted.getContent()).isEqualTo("Today's applications..."); }
4. @DynamicPropertySource
Spring Framework 5.2.5 ввел аннотацию @DynamicPropertySource для облегчения добавления свойств с динамическими значениями . Все, что нам нужно сделать, это создать статический метод с аннотацией @DynamicPropertySource и иметь только один экземпляр DynamicPropertyRegistry в качестве входных данных:
@SpringBootTest @Testcontainers public class ArticleLiveTest { @Container static PostgreSQLContainer> postgres = new PostgreSQLContainer<>("postgres:11") .withDatabaseName("prop") .withUsername("postgres") .withPassword("pass") .withExposedPorts(5432); @DynamicPropertySource static void registerPgProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", () -> String.format("jdbc:postgresql://localhost:%d/prop", postgres.getFirstMappedPort())); registry.add("spring.datasource.username", () -> "postgres"); registry.add("spring.datasource.password", () -> "pass"); } // tests are same as before }
Как показано выше, мы используем метод add(String, Supplier) в данной DynamicPropertyRegistry для добавления некоторых свойств в среду Spring |. Этот подход намного чище по сравнению с инициализатором, который мы видели ранее. Обратите внимание, что методы, аннотированные @DynamicPropertySource , должны быть объявлены как static и должны принимать только один аргумент типа DynamicPropertyRegistry .
В принципе, основная мотивация аннотации @DynmicPropertySource заключается в том, чтобы легче облегчить то, что уже было возможно. Хотя он изначально был разработан для работы с тестовыми контейнерами, его можно использовать везде, где нам нужно работать с динамическими конфигурациями.
5. Альтернатива: Испытательные Приспособления
До сих пор в обоих подходах настройка прибора и тестовый код тесно переплетены . Иногда эта тесная связь двух проблем усложняет тестовый код, особенно когда у нас есть несколько вещей, которые нужно настроить. Представьте, как выглядела бы настройка инфраструктуры, если бы мы использовали PostgreSQL и Apache Kafka в одном тесте.
В дополнение к этому настройка инфраструктуры и применение динамических конфигураций будут дублироваться во всех тестах, которые в них нуждаются .
Чтобы избежать этих недостатков, мы можем использовать средства тестирования приспособлений, которые предоставляет большинство фреймворков тестирования|/. Например, в JUnit 5 мы можем определить расширение |, которое запускает экземпляр PostgreSQL перед всеми тестами, настраивает Spring Boot и останавливает экземпляр PostgreSQL после выполнения тестов:
public class PostgreSQLExtension implements BeforeAllCallback, AfterAllCallback { private PostgreSQLContainer> postgres; @Override public void beforeAll(ExtensionContext context) { postgres = new PostgreSQLContainer<>("postgres:11") .withDatabaseName("prop") .withUsername("postgres") .withPassword("pass") .withExposedPorts(5432); postgres.start(); String jdbcUrl = String.format("jdbc:postgresql://localhost:%d/prop", postgres.getFirstMappedPort()); System.setProperty("spring.datasource.url", jdbcUrl); System.setProperty("spring.datasource.username", "postgres"); System.setProperty("spring.datasource.password", "pass"); } @Override public void afterAll(ExtensionContext context) { postgres.stop(); } }
Здесь мы реализуем После всех обратных вызовов | и Перед всеми обратными вызовами для создания расширения JUnit 5. Таким образом, JUnit 5 выполнит логику before All() перед запуском всех тестов и логику в методе after All() после запуска тестов. При таком подходе наш тестовый код будет таким же чистым, как и:
@SpringBootTest @ExtendWith(PostgreSQLExtension.class) public class ArticleTestFixtureLiveTest { // just the test code }
В дополнение к тому, что мы более удобочитаемы, мы можем легко повторно использовать ту же функциональность, просто добавив @ExtendWith(PostgreSQLExtension.class) аннотация. Нет необходимости копировать и вставлять всю настройку PostgreSQL везде, где она нам нужна, как мы делали в двух других подходах.
6. Заключение
В этом уроке мы впервые увидели, насколько сложно протестировать компонент Spring, который зависит от чего-то вроде базы данных. Затем мы представили три решения этой проблемы, каждое из которых улучшало то, что предлагало предыдущее решение.
Как обычно, все примеры доступны на GitHub .