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.
Автор: a_belyaev