1. Обзор
С помощью Spring Data JPA можно легко создавать запросы к БД и тестировать их с помощью встроенной базы данных H2.
Но иногда тестирование на реальной базе данных намного более полезно, особенно если мы используем запросы, привязанные к конкретной реализации БД.
В этом руководстве мы покажем, как использовать Testcontainers для интеграционного тестирования со Spring Data JPA и базой данных PostgreSQL.
В предыдущей статье мы создали несколько запросов к БД, используя в основном аннотацию @Query, которые мы сейчас и протестируем.
2. Конфигурация
Чтобы использовать в наших тестах базу данных PostgreSQL, мы должны добавить зависимость Testcontainers только тестов и драйвер PostgreSQL в наш файл pom.xml:
<dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <version>1.10.6</version> <scope>test</scope> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.2.5</version> </dependency>
Также создадим в каталоге ресурсов тестирования файл application.properties, в котором мы зададим для Spring использование нужного класса драйвера, а также создание и удаление схемы БД при каждом запуске теста:
spring.datasource.driver-class-name=org.postgresql.Driver spring.jpa.hibernate.ddl-auto=create-drop
3. Единичный тест
Чтобы начать использовать экземпляр PostgreSQL в классе с одним тестом, необходимо создать определение контейнера, а затем использовать его параметры для установления соединения:
@RunWith(SpringRunner.class) @SpringBootTest @ContextConfiguration(initializers = {UserRepositoryTCIntegrationTest.Initializer.class}) public class UserRepositoryTCIntegrationTest extends UserRepositoryCommonIntegrationTests { @ClassRule public static PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer("postgres:11.1") .withDatabaseName("integration-tests-db") .withUsername("sa") .withPassword("sa"); static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { public void initialize(ConfigurableApplicationContext configurableApplicationContext) { TestPropertyValues.of( "spring.datasource.url=" + postgreSQLContainer.getJdbcUrl(), "spring.datasource.username=" + postgreSQLContainer.getUsername(), "spring.datasource.password=" + postgreSQLContainer.getPassword() ).applyTo(configurableApplicationContext.getEnvironment()); } } }
В приведенном выше примере мы использовали @ClassRule из JUnit для настройки контейнера базы данных перед исполнением методов теста. Мы также создали статический внутренний класс, реализующий ApplicationContextInitializer. Наконец, мы применили аннотацию @ContextConfiguration к нашему тестовому классу с инициализирующим классом в качестве параметра.
Выполнив эти три действия, мы можем задать параметры соединения до публикации контекста Spring.
Теперь используем два запроса UPDATE из предыдущей статьи:
@Modifying @Query("update User u set u.status = :status where u.name = :name") int updateUserSetStatusForName(@Param("status") Integer status, @Param("name") String name); @Modifying @Query(value = "UPDATE Users u SET u.status = ? WHERE u.name = ?", nativeQuery = true) int updateUserSetStatusForNameNative(Integer status, String name);
И протестируем в настроенной среде исполнения:
@Test @Transactional public void givenUsersInDB_WhenUpdateStatusForNameModifyingQueryAnnotationJPQL_ThenModifyMatchingUsers(){ insertUsers(); int updatedUsersSize = userRepository.updateUserSetStatusForName(0, "SAMPLE"); assertThat(updatedUsersSize).isEqualTo(2); } @Test @Transactional public void givenUsersInDB_WhenUpdateStatusForNameModifyingQueryAnnotationNative_ThenModifyMatchingUsers(){ insertUsers(); int updatedUsersSize = userRepository.updateUserSetStatusForNameNative(0, "SAMPLE"); assertThat(updatedUsersSize).isEqualTo(2); } private void insertUsers() { userRepository.save(new User("SAMPLE", "email@example.com", 1)); userRepository.save(new User("SAMPLE1", "email2@example.com", 1)); userRepository.save(new User("SAMPLE", "email3@example.com", 1)); userRepository.save(new User("SAMPLE3", "email4@example.com", 1)); userRepository.flush(); }
В приведенном выше сценарии первый тест заканчивается успешно, а второй выдает InvalidDataAccessResourceUsageException с сообщением:
Caused by: org.postgresql.util.PSQLException: ERROR: column "u" of relation "users" does not exist
Если бы мы запускали те же самые тесты с использованием встроенной базы данных H2, оба были бы успешно завершены, но PostgreSQL не принимает алиасы в выражении SET. Мы можем быстро поправить запрос, удалив проблемный алиас:
@Modifying @Query(value = "UPDATE Users u SET status = ? WHERE u.name = ?", nativeQuery = true) int updateUserSetStatusForNameNative(Integer status, String name);
На этот раз оба теста пройдены успешно. В этом примере мы использовали Testcontainers для выявления проблемы с нативным запросом, которая в противном случае была бы обнаружена только после перехода на рабочую базу данных. Также следует заметить, что использование запросов JPQL в целом безопаснее, поскольку Spring правильно их переводит в зависимости от используемого провайдера базы данных.
4. Общий экземпляр базы данных
В предыдущем разделе мы описали, как использовать Testcontainers в единичном тесте. В реальных кейсах хотелось бы использовать один и тот же БД-контейнер в нескольких тестах из-за относительно длительного времени запуска.
Создадим общий класс для создания контейнера базы данных, унаследовав PostgreSQLContainer и переопределив методы start () и stop ():
public class BaeldungPostgresqlContainer extends PostgreSQLContainer<BaeldungPostgresqlContainer> { private static final String IMAGE_VERSION = "postgres:11.1"; private static BaeldungPostgresqlContainer container; private BaeldungPostgresqlContainer() { super(IMAGE_VERSION); } public static BaeldungPostgresqlContainer getInstance() { if (container == null) { container = new BaeldungPostgresqlContainer(); } return container; } @Override public void start() { super.start(); System.setProperty("DB_URL", container.getJdbcUrl()); System.setProperty("DB_USERNAME", container.getUsername()); System.setProperty("DB_PASSWORD", container.getPassword()); } @Override public void stop() { //do nothing, JVM handles shut down } }
Оставляя метод stop() пустым, мы даем возможность JVM самостоятельно обработать завершение работы контейнера. Мы также реализуем простой singleton, в котором только первый тест запускает контейнер, а каждый последующий тест использует существующий экземпляр. В методе start() мы используем System#setProperty для сохранение параметров соединения в переменные среды.
Теперь мы можем записать их в файл application.properties:
spring.datasource.url=${DB_URL} spring.datasource.username=${DB_USERNAME} spring.datasource.password=${DB_PASSWORD}
Теперь используем наш служебный класс в определении теста:
@RunWith(SpringRunner.class) @SpringBootTest public class UserRepositoryTCAutoIntegrationTest { @ClassRule public static PostgreSQLContainer postgreSQLContainer = BaeldungPostgresqlContainer.getInstance(); // tests }
Как и в предыдущих примерах, мы применили аннотацию @ClassRule к полю с определением контейнера. Таким образом, параметры подключения DataSource заполняются правильными значениями до создания контекста Spring.
Теперь мы можем реализовать несколько тестов, используя один и тот же экземпляр базы данных, просто задав поле с аннотацией @ClassRule, созданное с помощью нашего служебного класса BaeldungPostgresqlContainer.
5. Заключение
В этой статье мы показали методы тестирования на рабочей базе данных с помощью Testcontainers.
Ещё рассмотрели примеры использования единичного теста с помощью мех��низма ApplicationContextInitializer из Spring, а также реализации класса для многократного использования экземпляра базы данных.
Мы также показали, как Testcontainers может помочь в выявлении проблем совместимости между несколькими поставщиками баз данных, особенно для нативных запросов.
Как всегда, полный код, используемый в этой статье, доступен на GitHub.
