Spring Test Containers как бины

от автора

  1. Введение

  2. Теория

  3. Практика

  4. Заключение

  5. Ссылки

Введение

TestContainers довольно мощная штука, почитать о том, что это такое, зачем нужно можно на оф сайте, а так же есть интересная статья на Хабре.

Традиционная настройка через junit4 (посмотреть, как это делается можно в статьях в этой статье, этой статье), а более красивая через junit5 с использованием DynamicPropertySource (можно посмотреть в данной статье) довольно удобны в том случае, когда у нас простые контейнеры, никак не связанные между собой.

Трудности возникают в том случае, когда появляется потребность провести тестирование, например, базы + очереди, нескольких очередей и т.д., в общем, когда необходимо хотим провести сложное интеграционное тестирование.

Проблема состоит в том, чтобы определить последовательность старта контейнеров, динамически сформировать properties для контейнеров, которые зависят друг от друга.

И в этом случае можно воспользоваться имеющимся механизмом spring-beans – dependsOn.

Теория

Разбирать будем на простом примере тестовый контекст + тестконтейнер + определение параметров для подключения к БД в тестовом контексте.

Spring предоставляет разные возможности для того, чтобы настроить контекст. Этим мы и воспользуемся для того чтобы:

  1. Определить Bean с контейнером

  2. Определить момент запуска контейнера

  3. Динамически заполнить «.properties» файл значениями для подключения к БД ДО того как приложение начнёт пытаться подключиться к этой самой БД

  4. И сделаем так, чтобы это вся работа выполнялась только при наличии аннотации

Для достижения этих целей будет использоваться механизм BeanFactoryPostProcessor (как дань уважения Евгению Борисову 😉 ), т.к. благодаря BFPP возможно взаимодействовать с BeanDefinition’ами до того, как по ним начнут строиться бины. Этот механизм как раз будет использоваться для того, чтобы задать порядок старта контейнеров.

Практика

1.       Определяем бин с контейнером

Описываем класс, в котором указываем что за контейнер нам нужен: указываем image, логин, пароль, указываем порт (чтобы можно было локально приконнектиться при дебаге)

public class PostgresContainer {      private final PostgreSQLContainer<?> postgreSQLContainer;      public PostgresContainer() {         postgreSQLContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:14.3"))3"))                 .withDatabaseName("postgres")                 .withUsername("postgres")                 .withPassword("example")                 .withExposedPorts(5432)         ;     } }

2.       Определяем момент запуска контейнера

Запускать будем в PostConstruct.

Затем заполним конфиг файл для подключения к бд данными из нашего контейнера.

При разрушении контеста тоже пропишем логику отключения контейнера.

В итоге получится класс

public class PostgresContainer {      private final PostgreSQLContainer<?> postgreSQLContainer;      public PostgresContainer() {         postgreSQLContainer = new PostgreSQLContainer<>(DockerImageName.parse("postgres:14.3"))                 .withDatabaseName("postgres")                 .withUsername("postgres")                 .withPassword("example")                 .withExposedPorts(5432)         ;     }      @PostConstruct     public void start() {         postgreSQLContainer.start();          System.setProperty("spring.datasource.url", postgreSQLContainer.getJdbcUrl());         System.setProperty("spring.datasource.username", postgreSQLContainer.getUsername());         System.setProperty("spring.datasource.password", postgreSQLContainer.getPassword());     }      @PreDestroy     public void stop() {         postgreSQLContainer.stop();     } }

 

3.       Теперь нам нужно сделать так, чтобы наш контейнер стартовал сам по себе во время старта контекста.

Для этого создадим аннотацию, в которой создадим поле с названием того бина, который должен зависеть от нашего контейнера, таким образом определяем порядок построения бинов.

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface AutoConfigurePostgresContainer {      /**      * Название бина, в BeanDefinition которому пропишется зависимость на #{@link PostgresContainer}      */     String beanNameThatNeedsToDependOnContainer() default "";      /**      * Класс бина, в BeanDefinition которому пропишется зависимость на #{@link PostgresContainer}      */     Class<?> beanClassNameThatNeedsToDependOnContainer() default Void.class;  }

Тут конечно можно пойти дальше, создать такое поле, которое будет принимать название бина с самим контейнером (это нужно в случае, если у нас несколько таких контейнеров). В результате получится аннотация, содержащая название бина с контейнером и название бина, который должен зависеть от этого контейнера. Таким образом получится очень гибко настроить порядок старта бинов. Это нужно, например, в случае если один контейнер настраивается такими значениями, которые формирует другой контейнер и т.п.

4.       Определим BFPP который будет работать с этой аннотацией

public class DependsOnContainerSetterBFPP implements BeanFactoryPostProcessor, Ordered {      private final AutoConfigurePostgresContainer annotation;      public DependsOnContainerSetterBFPP(AutoConfigurePostgresContainer annotation) {         this.annotation = annotation;     }      @Override     public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {         registerContainer(beanFactory);         setDependsOn(beanFactory, annotation);     }      /**      * Регистрируется класс #{@link PostgresContainer}      */     private void registerContainer(ConfigurableListableBeanFactory beanFactory) {         BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;         registry.registerBeanDefinition(decapitalize(PostgresContainer.class.getSimpleName()),                 BeanDefinitionBuilder.genericBeanDefinition(PostgresContainer.class).getBeanDefinition());     }      /**      * Заставляем класс {@code annotation.beanToSubscribeOnEnvironmentSetter}      * зависеть от {@link PostgresContainer}      */     private void setDependsOn(ConfigurableListableBeanFactory beanFactory, AutoConfigurePostgresContainer annotation) {         beanFactory.getBeanDefinition(geBeanWhoNeedsToDepend(annotation))                 .setDependsOn(decapitalize(PostgresContainer.class.getSimpleName()));      }      /**      * @return название бина, который надо подписать на {@link PostgresContainer}      * Название бина достаётся либо из {@param annotation.beanNameThatNeedsToDependOnContainer}      * либо из класса {@param annotation.beanClassNameThatNeedsToDependOnContainer}      * название которого переводится в стрингу и понижается первая буква      */     private String geBeanWhoNeedsToDepend(AutoConfigurePostgresContainer annotation) {         String beanToSubscribe = annotation.beanNameThatNeedsToDependOnContainer();         if (!beanToSubscribe.isEmpty()) {             return beanToSubscribe;         }         Class<?> beanClassToSubscribe = annotation.beanClassNameThatNeedsToDependOnContainer();         if (beanClassToSubscribe != Void.class) {             return decapitalize(beanClassToSubscribe.getSimpleName());         }         throw new RuntimeException();     }      /**      * Порядок выставляем самый приоритетный, чтобы зависимость прописалась раньше всего      * это не обязательно, но для большего контроля можно добавить      */     @Override     public int getOrder() {         return 0;     } }

5.       Далее, т.к. контекст у нас тестовый – надо сделать так, чтобы тестовый контекст распознавал нашу аннотацию и делал всю нужную магию. Делается это через кастомизатор контекста, который вызывается фабрикой кастомизаторов контекста (ContextCustomizerFactory -> ContextCustomizer).
Создаём ContextCustomizerFactory, в котором читаем аннотацию и передаём в кастомизатор контекста (который будет создан на след. Шаге)

public class ContainerContextCustomizerFactory implements ContextCustomizerFactory {      @Override     public ContextCustomizer createContextCustomizer(       Class<?> testClass,       List<ContextConfigurationAttributes> configAttributes     ) {         AutoConfigurePostgresContainer annotation = testClass.getAnnotation(AutoConfigurePostgresContainer.class);         return new ContainerContextCustomizer(annotation);     }  }

Прописываем созданный класс в META-INF/spring.factories

org.springframework.test.context.ContextCustomizerFactory=\   путь.к.классу.ContainerContextCustomizerFactory

6.       Создаём кастомизатор контекста. В котором регистрируем BFPP .

public class ContainerContextCustomizer implements ContextCustomizer {      private final AutoConfigurePostgresContainer annotation;     public ContainerContextCustomizer(AutoConfigurePostgresContainer annotation) {         this.annotation = annotation;     }      @Override     public void customizeContext(       ConfigurableApplicationContext context,        MergedContextConfiguration mergedConfig     ) {         context.addBeanFactoryPostProcessor(new DependsOnContainerSetterBFPP(annotation));     }  }

7.       Запускаем тест

@SpringBootTest @AutoConfigurePostgresContainer(beanClassNameThatNeedsToDependOnContainer = DataSource.class) class DatabaseTest {      @Autowired     private PostgresContainer postgresContainer;      @Autowired     private MyRepository repository;      @Test     void testConnect() {         assertTrue(postgresContainer.getPostgreSQLContainer().isRunning());     }      @Test     void testRepo() {         MyEntity testEntity = new MyEntity().setName("testName");         repository.save(testEntity);          List<MyEntity> all = repository.findAll();         assertThat(testEntity).isEqualTo(all.get(0));     }  }

Разумеется, всё можно кастомизировать и добавлять свой функционал.

Заключение

Теперь не имеет значения какой версии junit используется в тестах, настраиваются контейнеры одинаково.

Было рассмотрено:

  1. Кастомизация тестового контекста через ContextCustomizer

  2. Взаимодействие с BeanDefinition’ами на раннем этапе инициализации этого контекста

  3. Реализация аннотации и взаимодействия тестового контекста с этой аннотацией

Представленный функционал позволяет покрыть тестами более широкую интеграционную часть приложения, что увеличивает качество кода и снижает количество ошибок, которые возникают при регресс тестировании и которые связанны с интеграционной частью.

Ссылки

Что такое тестконтейнер

Интеграционные тесты баз данных с помощью Spring Boot и Testcontainers

Как собрать образ Oracle DB для Testcontainers

Интеграционное тестирование в SpringBoot с TestContainers-стартером

Обзор модульного и интеграционного тестирования Spring Boot

Интеграционные тесты баз данных с помощью Spring Boot и Testcontainers

Мой репозиторий с настроенным spring bean testcontainer’ом  


ссылка на оригинал статьи https://habr.com/ru/post/681232/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *