Автор оригинала: Jędrzej Frankowski.
1. Обзор
Spring Data JPA предоставляет простой способ создания запросов к базе данных и их тестирования с помощью встроенной базы данных H2.
Но в некоторых случаях тестирование на реальной базе данных намного выгоднее, особенно если мы используем запросы, зависящие от поставщика.
В этом уроке мы продемонстрируем как использовать Тестовые контейнеры для интеграционного тестирования с Spring Data JPA и базой данных PostgreSQL.
В нашем предыдущем уроке мы создали несколько запросов к базе данных, используя в основном аннотацию @Query , которую мы сейчас проверим.
2. Конфигурация
Чтобы использовать базу данных PostgreSQL в наших тестах, мы должны добавить зависимость Test containers с test scope и драйвер PostgreSQL в наш pom.xml :
org.testcontainers postgresql 1.10.6 test org.postgresql postgresql 42.2.5
Давайте также создадим файл 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{ 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.
Теперь давайте используем два запроса на ОБНОВЛЕНИЕ из предыдущей статьи:
@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 protected]", 1)); userRepository.save(new User("SAMPLE1", "[email protected]", 1)); userRepository.save(new User("SAMPLE", "[email protected]", 1)); userRepository.save(new User("SAMPLE3", "[email protected]", 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);
На этот раз оба теста завершаются успешно. В этом примере мы использовали тестовые контейнеры для выявления проблемы с собственным запросом, которая в противном случае была бы обнаружена после переключения на реальную производственную базу данных. Мы также должны заметить, что использование запросов JPQL в целом безопаснее, поскольку Spring правильно переводит их в зависимости от используемого поставщика базы данных.
4. Общий экземпляр Базы данных
В предыдущем параграфе мы описали, как использовать тестовые контейнеры в одном тесте. В реальном сценарии мы хотели бы повторно использовать один и тот же контейнер базы данных в нескольких тестах из-за относительно длительного времени запуска.
Теперь давайте создадим общий класс для создания контейнера базы данных, расширив PostgreSQLContainer и переопределив методы start() и stop() :
public class BaeldungPostgresqlContainer extends PostgreSQLContainer{ 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 обрабатывать завершение работы контейнера. Мы также реализуем простой одноэлементный шаблон, в котором только первый тест запускает запуск контейнера, а каждый последующий тест использует существующий экземпляр. В методе 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 к полю, содержащему определение контейнера. Таким образом, свойства Источника данных соединения заполняются правильными значениями перед созданием контекста Spring.
Теперь мы можем реализовать несколько тестов, используя один и тот же экземпляр базы данных , просто определив @ClassRule аннотированное поле, созданное с помощью нашего служебного класса BaeldungPostgresqlContainer|/.
5. Заключение
В этой статье мы проиллюстрировали способы выполнения тестов на реальном экземпляре базы данных с использованием тестовых контейнеров.
Мы рассмотрели примеры использования одного теста с использованием механизма ApplicationContextInitializer из Spring, а также реализации класса для повторного создания экземпляра базы данных.
Мы также показали, как эти контейнеры могут помочь в выявлении проблем совместимости между несколькими поставщиками баз данных, особенно для собственных запросов.
Как всегда, полный код, используемый в этой статье, доступен на GitHub .