Рубрики
Без рубрики

Руководство по @DynamicPropertySource весной

Узнайте, как использовать @DynamicPropertySource для управления тестированием с помощью внешних приложений.

Автор оригинала: 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 и Тестовые контейнеры , легко реализовать третий вариант.

Вот как будет выглядеть наш рабочий процесс тестирования, если мы будем использовать такие технологии, как тестовые контейнеры:

  1. Настройте компонент, такой как PostgreSQL, перед всеми тестами. Обычно эти компоненты прослушивают случайные порты.
  2. Проведите тесты.
  3. Снесите компонент.

Если наш контейнер 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 .