
Введение
TestContainers довольно мощная штука, почитать о том, что это такое, зачем нужно можно на оф сайте, а так же есть интересная статья на Хабре.
Традиционная настройка через junit4 (посмотреть, как это делается можно в статьях в этой статье, этой статье), а более красивая через junit5 с использованием DynamicPropertySource (можно посмотреть в данной статье) довольно удобны в том случае, когда у нас простые контейнеры, никак не связанные между собой.
Трудности возникают в том случае, когда появляется потребность провести тестирование, например, базы + очереди, нескольких очередей и т.д., в общем, когда необходимо хотим провести сложное интеграционное тестирование.
Проблема состоит в том, чтобы определить последовательность старта контейнеров, динамически сформировать properties для контейнеров, которые зависят друг от друга.
И в этом случае можно воспользоваться имеющимся механизмом spring-beans – dependsOn.
Теория
Разбирать будем на простом примере тестовый контекст + тестконтейнер + определение параметров для подключения к БД в тестовом контексте.
Spring предоставляет разные возможности для того, чтобы настроить контекст. Этим мы и воспользуемся для того чтобы:
-
Определить Bean с контейнером
-
Определить момент запуска контейнера
-
Динамически заполнить «.properties» файл значениями для подключения к БД ДО того как приложение начнёт пытаться подключиться к этой самой БД
-
И сделаем так, чтобы это вся работа выполнялась только при наличии аннотации
Для достижения этих целей будет использоваться механизм 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 используется в тестах, настраиваются контейнеры одинаково.
Было рассмотрено:
-
Кастомизация тестового контекста через ContextCustomizer
-
Взаимодействие с BeanDefinition’ами на раннем этапе инициализации этого контекста
-
Реализация аннотации и взаимодействия тестового контекста с этой аннотацией
Представленный функционал позволяет покрыть тестами более широкую интеграционную часть приложения, что увеличивает качество кода и снижает количество ошибок, которые возникают при регресс тестировании и которые связанны с интеграционной частью.
Ссылки
Интеграционные тесты баз данных с помощью Spring Boot и Testcontainers
Как собрать образ Oracle DB для Testcontainers
Интеграционное тестирование в SpringBoot с TestContainers-стартером
Обзор модульного и интеграционного тестирования Spring Boot
Интеграционные тесты баз данных с помощью Spring Boot и Testcontainers
ссылка на оригинал статьи https://habr.com/ru/post/681232/
Добавить комментарий