Интеграционное тестирование, если у вас R2DBC и liquibase

от автора

Уже немало копий поломали в поиске грааля идеального способа интеграционного тестирования с использованием БД.

Вашему вниманию предлагается способ решения этой проблемы самым минималистичным способом без необходимости создавать угрозы безопасности вашего кластера или создавать разработчикам невыносимые условия труда. Впрочем что может быть хуже сломанной кофе-машины или отсутствия лавандового рафа?

Постановка задачи

Необходимо обеспечить возможность запуска интеграционных тестов БД с использованием r2dbc так, что бы удовлетворять следующим условиям:

  1. Выбор образа БД на основе тэгов докер образов;

  2. Запуск локально при помощи testcontainers (docker);

  3. Запуск пайплайнов в gitlab-runner без использования docker:dind (полный запрет на привилигированный доступ из контейнера).

По мнению автора максимально близко приблизился к желаемому результату некий Овечкин: «Spring Test Containers как бины». Разумеется у него не обошлось без фатального недостатка, и проблема не только в другой вере использовании datasource, но так же нагромождении ненужного кода и усложнения. Автор не разделяет мнение, что раз сеньор, то сиди и страдай на костыле, а джуну только аннотацию поставь и ни о чем не думай.

Предлагается доработать его решение до библиотеки, которую может и нельзя выставлить в виде некоего spring-boot-testcontainers-starter-gitlab-edition, но общей библиотеки ваших микросервисов, которую будет достаточно «просто подключить в testImplementation» там, где необходимо.

Решение

В Spring boot присутствуют авто-конфигурации, предлагается использовать их для получения нужного результата, пример такой конфигурации:

@Slf4j @ConditionalOnProperty(name = "testcontainers.db.enabled", havingValue = "true", matchIfMissing = true) @AutoConfiguration @EnableConfigurationProperties({R2dbcProperties.class, DbTestContainerConfig.class}) public class DbTestContainerAutoConfiguration implements BeanFactoryPostProcessor {      @Override     public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {         beanFactory.getBeanDefinition(decapitalize(ConnectionFactory.class.getSimpleName()))                 .setDependsOn(decapitalize(PostgresContainer.class.getSimpleName()));     }      @Bean     public PostgresContainer postgresContainer(             DbTestContainerConfig dbTestContainerConfig,             R2dbcProperties r2dbcProperties     ) {         return new PostgresContainer(dbTestContainerConfig, r2dbcProperties);     } }

Данная конфигурация отключаема через настройку testcontainers.db.enabled , что позволит нам в будущем не запускать ее при работе gitlab-runner. Разумеется, если вам удобно, то можете сидеть на dind, тогда возможность отключать конфигурацию вам не нужна.

Прямая зависимость ConnectionFactory от нашего тест-контейнера позволит нам предоставить для фабрики наш собственный набор настроек подключения, для этого методом научного ctrl+c/ctrl+v получим в бине, реализующем интерфейс R2dbcConnectionDetails следующий метод:

    @Override     public ConnectionFactoryOptions getConnectionFactoryOptions() {         String connectionUrl = getConnectionUrl();         ConnectionFactoryOptions urlOptions = ConnectionFactoryOptions.parse(connectionUrl);         ConnectionFactoryOptions.Builder optionsBuilder = urlOptions.mutate();         configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.USER, postgreSQLContainer::getUsername, StringUtils::hasText);         configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.PASSWORD, postgreSQLContainer::getPassword, StringUtils::hasText);         configureIf(optionsBuilder, urlOptions, ConnectionFactoryOptions.DATABASE, postgreSQLContainer::getDatabaseName, StringUtils::hasText);         if (this.properties.getProperties() != null) {             this.properties.getProperties()                     .forEach((key, value) -> optionsBuilder.option(Option.valueOf(key), value));         }         log.debug("Configured r2dbc [url: {}]", connectionUrl);         return optionsBuilder.build();     }

Это позволит нам предоставить для ConnectionFactory наш собственный набор параметров подключения, переопределяя имеющиеся.

Теперь, используя эту библиотеку, мы можем подключить ее к нашему проекту, задать желаемый образ в application.yml:

testcontainers:   db:     image: "registry.nexus.dev.lo/testcontainers/database-liquibase:dev"

И писать тесты с полностью рабочей БД в нашем тестовом окружении без необходимости задавать какие-то лишние аннотации:

@SpringBootTest class PostgreNoteDaoTest {     @Autowired     PostgreNoteDao dao;      @Test     void test_save_read() {         StepVerifier.create(dao.save(Note.builder()                         .title("Example")                         .content("Hello")                         .build()))                 .expectSubscription()                 .assertNext(note -> {                     assertEquals(1L, note.getId());                     assertNotNull(note.getCreatedAt());                     assertNotNull(note.getUpdatedAt());                     assertEquals(note.getCreatedAt(), note.getUpdatedAt());                 })                 .verifyComplete();          StepVerifier.create(dao.find(1L))                 .expectSubscription()                 .assertNext(note -> {                     assertEquals(1L, note.getId());                     assertEquals("Example", note.getTitle());                     assertEquals("Hello", note.getContent());                     assertNotNull(note.getCreatedAt());                     assertNotNull(note.getUpdatedAt());                     assertEquals(note.getCreatedAt(), note.getUpdatedAt());                 })                 .verifyComplete();     } } 

Если запуск SpringBootTest вам не подходит, то можно всегда импортировать авто-конфигурацию, она подключит для вас БД, однако в этом случае каждый импорт будет запускать свой инстанс БД.

Возможность локального запуска интеграционных тестов есть. Приступим к gitlab-runner.

Настраиваем Gitlab CI

Как описано выше по религиозными соображениям в связи с проблемами в безопасности, использование docker:dind — невозможно. Для того, что бы запускать интеграционные тесты в пайплайне будет достаточно, если мы предоставим для тестов параметры подключения. Например так:

build:   variables:     GIT_DEPTH: 1     GRADLE_OPTS: >       -Dorg.gradle.daemon=false       -Dorg.gradle.caching=true       -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false     DB_IMAGE: "registry.nexus.dev.lo/testcontainers/database-liquibase"     DB_TAG: "dev"     POSTGRES_DB: postgres     POSTGRES_USER: user     POSTGRES_PASSWORD: password     PGPASSWORD: password     POSTGRES_SCHEMA: public     POSTGRES_PORT: 5432     TESTCONTAINERS_DB_ENABLED: "false"     SPRING_R2DBC_URL: "r2dbc:postgresql://test-database:${POSTGRES_PORT}/${POSTGRES_DB}?currentSchema=${POSTGRES_SCHEMA}&sslmode=disable&binary_parameters=yes"     SPRING_R2DBC_USERNAME: "${POSTGRES_USER}"     SPRING_R2DBC_PASSWORD: "${POSTGRES_PASSWORD}"   services:     - name: "${DB_IMAGE}:${DB_TAG}"       alias: test-database   stage: build   rules:     - if: $CI_COMMIT_REF_NAME =~ /^(bugfix|hotfix|feature|dev).*$/   script:     - gradle clean assemble check

Обратите внимание, что переменной TESTCONTAINERS_DB_ENABLED: «false» мы запрещаем запускаться тестконтейнерам.

Вместо этого мы настраиваем сервис согласно тому образу, который разработчик указывает в переменных DB_IMAGE:DB_TAG. Очевидно, что их можно некоторым образом брать из ветки/файла или названия MR, но лучше остановиться на этом. Образы БД редко меняются и обычно достаточно текущей dev версии. Если разработка требует времени и частого запуска — всегда можно отредактировать переменную, а сами изменения внести в отдельный ченжсет в Intellij Idea. Так вы случайно не закоммитите нежелательные результаты. Да и ревью никто не отменял. Что еще делает не dev тэг в вашем МР, сударь?

Примечание: если у вас мультимодульный проект, то запуск тестов будет отличаться. Для тест-контейнеров БД будет создаваться на каждый модуль, в то время как в пайплайне на всю сессию тестирования. Нужно учитывать этот факт при написании тестов.

Бонус: Как нам свой образ БД построить

Очевидно, что образ БД можно всегда делать руками, достаточно простого Dockerfile:

FROM registry.nexus.dev.lo/lib/postgres:15-alpine COPY database.sql /docker-entrypoint-initdb.d/

Всего-то нужно сделать дамп желаемой БД в файл database.sql и образ у вас. Но ведь нам требуется автоматизированное решение, которое позволит создавать образы без лишних усилий.

Для начала нам потребуется агент:

FROM registry.nexus.dev.lo/lib/postgres:15-alpine AS postgres-image FROM gradle:8.5-jdk17-alpine AS gradle COPY --from=postgres-image /usr/local /usr/local/ COPY --from=postgres-image /usr/lib /usr/lib/ COPY --from=postgres-image /lib /lib/

Если у кого есть более хороший вариант как получить комбо-образ для gradle + pg_dump — напишите в комментариях, а то образ толстоват — 900 мб.

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

Валидация liquibase проста:

validate:   stage: validate   variables:     POSTGRES_DB: postgres     POSTGRES_USER: user     POSTGRES_PASSWORD: password     PGPASSWORD: password     POSTGRES_SCHEMA: public     POSTGRES_PORT: 5432   services:     - name: registry.nexus.dev.lo/lib/postgres:15-alpine       alias: liquibase-postgres   artifacts:     paths:       - database.sql     expire_in: 1 day   script:     - >       gradle update validate       -Ddb.context=database,schema,test-data        -Ddb.server=liquibase-postgres        -Ddb.name=${POSTGRES_DB}        -Ddb.login=${POSTGRES_USER}        -Ddb.password=${POSTGRES_PASSWORD}        -Ddb.port=${POSTGRES_PORT}        -Ddb.schema=${POSTGRES_SCHEMA}     - pg_dump -p 5432 -h localhost --username=${POSTGRES_USER} -d "${POSTGRES_DB}" > database.sql   rules:     - if: $CI_COMMIT_REF_NAME =~ /^(dev).*$/     - if: $CI_MERGE_REQUEST_ID

В артефакты добавляем наш database.sql, что бы он был доступен задачи построеня образа.

Как известно, использовать docker:dind — нельзя, но собрать образ нужно, для этого существует аналог — buildah, он не требует никаких привилегий в системе и может работать в gitlab-runner как есть.

Пример базовой задачи для построения образа:

.build_image_base:   image: registry.nexus.dev.lo/lib/buildah:v1.34.0   stage: validate   needs:     - job: validate   variables:     NEXUS_REGISTRY: registry.nexus.dev.lo     BUILDAH_FORMAT: docker     STORAGE_DRIVER: vfs   before_script:     - echo "${CI_BOT_NEXUS_PASSWORD}" | buildah login --tls-verify=false -u "${CI_BOT_NEXUS_USERNAME}" --password-stdin ${NEXUS_REGISTRY}   script:     - buildah build --tls-verify=false -t "${IMAGE_NAME}:${IMAGE_VERSION}"     - buildah images     - buildah push --tls-verify=false "${IMAGE_NAME}:${IMAGE_VERSION}"

Не забудьте определить переменные CI_BOT_NEXUS_PASSWORD и CI_BOT_NEXUS_USERNAME в переменных проекта. Либо используйте регистр образов самого гитлаба.

Теперь имея эту базу, мы можем сделать два варианта для получения образов: для dev и для мерж реквестов:

build_image_dev:   extends: .build_image_base   variables:     IMAGE_VERSION: "dev"   rules:     - if: $CI_COMMIT_REF_NAME =~ /^(dev).*$/  build_image_feature:   extends: .build_image_base   variables:     IMAGE_VERSION: "MR-$CI_MERGE_REQUEST_ID"   rules:     - if: $CI_MERGE_REQUEST_ID       when: manual

Сделаем сборку из мерж реквестов запускаемой руками, так как эти образы не всегда могут быть нужны, а забивать регистр не стоит.

Теперь автоматизация образов БД так же в наличии.

Заключение

В статье рассмотрен способ унификации процесса интеграционного тестирования БД для R2DBC, предложен вариант автоматизации создания образов БД из ченжлога liquibase.

Ссылки на репозитории

  1. https://github.com/lastrix/database-liquibase построение образа БД из liquibase схемы;

  2. https://github.com/lastrix/note-app — библиотека и пример использования.


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


Комментарии

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

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