В этой статье мы оговорим о библиотеке Testcontainers и о том, как ее использовать для упрощения нашей жизни, когда дело доходит до интеграционного тестирования нашего кода. В приведенном примере я буду использовать простое приложение, работающее с рецензиями некоторых курсов.  По сути, приложение представляет собой сервис, который предоставляет несколько конечных точек GraphQL для создания, запроса и удаления рецензий из базы данных PostgreSQL через Spring Data R2DBC. Приложение написано на Kotlin с использованием Spring Boot 2.7.3.  Я решил написать эту статью специально для Spring Data R2DBC, поскольку в случае Spring Data JPA интеграционное тестирование с Testcontainers не вызывает затруднений. Тем не менее, когда дело доходит до R2DBC, возникают некоторые проблемы, которые необходимо решить. Итак, давайте перейдем к делу. Давайте рассмотрим наш домен. А вот его репозиторий: А вот и свойства подключения: Когда дело доходит до тестирования, Spring предлагает довольно простой способ создания для этой цели базы данных в памяти H2, но... всегда есть но. H2 имеет некоторые недостатки: Во-первых, обычно H2 не является производственной БД; в нашем случае мы используем PostgreSQL, и сложно поддерживать две схемы БД, одну для производственного использования, а другую для интеграционного тестирования, особенно если вы зависите от некоторых возможностей провайдера (запросов, функций, ограничений и так далее). Чтобы быть максимально уверенным в тестах, всегда рекомендуется максимально воспроизводить производственную среду, чего нельзя сказать о H2. Еще одним недостатком может быть возможность миграции производственной БД на другую базу данных - в этом случае H2, имея другую схему, не поймает никаких проблем, следовательно, будет ненадежен. Некоторые могут возразить, что для тестирования можно иметь отдельную выделенную БД на сервере, машине или даже в другой схеме. Тем не менее опять же, проблемы с поддержкой  схем и возможность конфликтов с изменениями/миграциями других разработчиков слишком велики. Testcontainers  — это библиотека Java, которая поддерживает тесты JUnit, предоставляя легковесные одноразовые экземпляры обычных баз данных, Веб-браузеров Selenium или чего-либо еще, что может работать в контейнере Docker.   С Testcontainers исчезают все недостатки, упомянутые выше: поскольку вы работаете с базой данных, похожей на производственную, вам не нужно поддерживать две или более различных отдельных схем. Вам не нужно беспокоиться о сценариях миграции, поскольку у вас будут неудачные тесты, и последнее, но не менее важное: вам не нужно беспокоиться об обслуживании другого сервера или виртуальной машины, так как Testcontainers и Docker позаботятся об этом. Вот зависимости, которые мы будем использовать: Существуют разные способы работы с Testcontainers в Spring Boot. Тем не менее, я покажу вам шаблон singleton-instance (одна база данных для всех тестов), поскольку гораздо быстрее один раз запустить экземпляр базы данных и позволить всем вашим тестам взаимодействовать с ней.  Чтобы это работало, я собираюсь создать абстрактный класс, содержащий все экземпляры Testcontainers и логику их создания при запуске.   Давайте внимательно посмотрим, что мы имеем. Здесь мы используем способность testcontainers извлекать образы Docker и, следовательно, создавать экземпляр контейнера базы данных.   Здесь мы переопределим свойства подключения R2DBC во время выполнения на те, которые указывают на наш вновь созданный контейнер.   Поскольку мы используем Spring Data R2DBC, spring.r2dbc.url требует немного больше внимания, чтобы быть правильно построенным, так как на данный момент PostgreSQLContainer предоставляет только метод getJdbcUrl(). И здесь мы запускаем наш контейнер перед всеми тестами.   Имея все это, мы готовы написать несколько тестов.   Что мы имеем здесь: @DataR2dbcTest — это аннотация к фрагменту запуска Spring Boot теста, которая может быть использована для теста R2DBC, сфокусированного только на компонентах Data R2DBC.  @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) — здесь мы говорим Spring не беспокоиться о настройке тестовой базы данных, поскольку мы собираемся сделать это сами. И поскольку у нас есть R2dbcRepository, который предоставляет данные реактивным способом с помощью Flux/ Mono, мы используем StepVerifier для создания проверяемого сценария для наших асинхронных последовательностей Publisher, выражая ожидания относительно событий, которые произойдут при подписке. Круто, правда? Давайте запустим это. io.r2dbc.postgresql.ExceptionFactory$PostgresqlBadGrammarException: [42P01] relation "reviews" does not exist Облом! Мы забыли позаботиться о нашей схеме и данных/записях. Но как же нам это сделать? В Spring Data JPA об этом можно позаботиться, используя следующее: А если поместить schema.sql в папку с DDL и data.sql с DML в папку src/main/resources, то все работает автоматически. Или, если у вас используется Flyway/Liquibase, есть и другие методы сделать это. Более того, в Spring Data JPA есть @Sql, который позволяет запускать различные .sql файлы перед тестовым методом, чего было бы достаточно для нашего случая. Но это не тот случай, мы используем Spring Data R2DBC, который на данный момент не поддерживает подобные функции, и у нас нет фреймворка миграции.  Таким образом, на наши плечи ложится ответственность за написание чего-то похожего на то, что предлагает Spring Data JPA, что будет достаточным и настраиваемым для легкого написания интеграционных тестов. Попробуем повторить аннотацию @Sql, создав аналогичную аннотацию @RunSql. Теперь нам нужно расширить функциональность нашего теста возможностью чтения этой аннотации и запуска предоставленных скриптов. Как же нам повезло, что в Spring уже есть кое-что именно для нашего случая, и называется оно BeforeTestExecutionCallback. Давайте почитаем документацию: BeforeTestExecutionCallback определяет API для расширений, которые должны обеспечить дополнительные действия для тестов непосредственно перед выполнением отдельного теста, но после выполнения любых пользовательских методов настройки (например, методов @BeforeEach) для этого теста. Это звучит правильно: давайте расширим его и переопределим метод beforeTestExecution. Итак, что мы здесь сделали:  Мы берем текущий метод теста из контекста расширения и проверяем аннотацию @RunSq. Мы берем текущий экземпляр теста — это экземпляр нашего запущенного теста. Мы передаем текущий экземпляр теста в getConnectionFactory, чтобы он мог получить @Autowired ConnectionFactory из нашего родителя, в нашем случае, абстрактного класса AbstractTestcontainersIntegrationTest.  Используя полученные connectionFactory, ScriptUtils,мы выполняем скрипты, найденные в аннотации @RunSql, в блокирующем режиме. Как я уже говорил, наш AbstractTestcontainersIntegrationTest нуждается в небольшом изменении: нам нужно добавить код @Autowired lateinit var connectionFactory: ConnectionFactory, чтобы он мог быть выбран нашим расширением.  Когда все готово, остается только использовать это расширение с помощью @ExtendWith(RunSqlExtension::class) и нашей новой аннотацией @RunSql. Вот как теперь выглядит наш тест. А это содержимое файла schema.sql из папки resources.   А вот содержимое файла reviews.sql из папки resources/data.   Пожалуйста, обратите внимание на create table if not exists в файле schema.sql и truncate reviews cascade в файле reviews.sql— это необходимо для обеспечения чистого состояния  базы данных для каждого теста. Теперь, если мы запустим наши тесты, все будет зеленым. Мы можем пойти дальше и сделать еще немного, чтобы следовать принципу DRY в отношении аннотаций, используемых в этом тесте, которые могут быть объединены в одну аннотацию, а затем использованы повторно, вот так. Вот, собственно, и все, друзья. Благодаря общему абстрактному классу, расширению тестов и пользовательской аннотации теперь каждый новый набор тестов для интеграции с базой данных должен быть проще простого.  Но подождите, зачем останавливаться на достигнутом? Имея нашу настройку, мы можем поиграть с компонентными тестами (широкими интеграционными тестами), например, для тестирования наших конечных точек — начиная с простого HTTP-вызова, проходя через все бизнес-слои и сервисы вплоть до уровня базы данных. Вот пример того, как это можно сделать для конечных точек GraphQL. И снова, если вы хотите повторно использовать все аннотации, просто создайте новую.   Вы можете найти код на GitHub. Удачного кодирования и написания тестов!