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 правильно переводит их в зависимости от используемого поставщика базы данных.
3.1. Одна база данных на тест с конфигурацией
До сих пор мы использовали правила JUnit 4 для запуска экземпляра базы данных перед запуском всех тестов внутри тестового класса. В конце концов, этот подход создаст экземпляр базы данных перед каждым тестовым классом и удалит его после выполнения всех тестов в каждом классе.
Такой подход создает максимальную изоляцию между тестовыми экземплярами . Кроме того, накладные расходы, связанные с многократным запуском базы данных, могут замедлить выполнение тестов.
В дополнение к подходу правил JUnit 4 мы можем изменить URL-адрес JDBC и указать Testcontainers создать экземпляр базы данных для каждого тестового класса . Этот подход будет работать, не требуя от нас написания инфраструктурного кода в наших тестах.
Например, чтобы переписать приведенный выше пример, все, что нам нужно сделать, это добавить это в наш application.properties
:
spring.datasource.url=jdbc:tc:postgresql:11.1:///integration-tests-db
« tc:»
заставит Testcontainers создавать экземпляры базы данных без каких-либо изменений кода. Итак, наш тестовый класс будет таким же простым, как:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class UserRepositoryTCJdbcLiveTest extends UserRepositoryCommon {
@Test
@Transactional
public void givenUsersInDB_WhenUpdateStatusForNameModifyingQueryAnnotationNative_ThenModifyMatchingUsers() {
// same as above
}
}
Если мы собираемся иметь один экземпляр базы данных для каждого тестового класса, этот подход является предпочтительным.
4. Общий экземпляр базы данных
В предыдущем абзаце мы описали, как использовать Testcontainers в одном тесте. В реальном сценарии мы хотели бы повторно использовать один и тот же контейнер базы данных в нескольких тестах из-за относительно длительного времени запуска.
Давайте теперь создадим общий класс для создания контейнера базы данных, расширив PostgreSQLContainer
и переопределив методы start()
и stop() :
public class ForEachPostgresqlContainer extends PostgreSQLContainer<ForEachPostgresqlContainer> {
private static final String IMAGE_VERSION = "postgres:11.1";
private static ForEachPostgresqlContainer container;
private ForEachPostgresqlContainer() {
super(IMAGE_VERSION);
}
public static ForEachPostgresqlContainer getInstance() {
if (container == null) {
container = new ForEachPostgresqlContainer();
}
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 обрабатывать отключение контейнера. Мы также реализуем простой одноэлементный шаблон, в котором только первый тест запускает контейнер, а каждый последующий тест использует существующий экземпляр. В методе 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 = ForEachPostgresqlContainer.getInstance();
// tests
}
Как и в предыдущих примерах, мы применили аннотацию @ClassRule
к полю, содержащему определение контейнера. Таким образом, свойства соединения DataSource
заполняются правильными значениями до создания контекста Spring.
Теперь мы можем реализовать несколько тестов с использованием одного и того же экземпляра базы данных, просто определив аннотированное поле @ClassRule
, созданное с помощью нашего служебного класса ForEachPostgresqlContainer .
5. Вывод
В этой статье мы проиллюстрировали способы выполнения тестов на реальном экземпляре базы данных с помощью Testcontainers.
Мы рассмотрели примеры использования одного теста с использованием механизма ApplicationContextInitializer
из Spring, а также реализации класса для повторного использования экземпляров базы данных.
Мы также показали, как Testcontainers могут помочь в выявлении проблем совместимости между несколькими поставщиками баз данных, особенно для собственных запросов.
Как всегда, полный код, использованный в этой статье, доступен на GitHub .