Статьи

TestContainers и Spring Boot

TestContainers просто потрясающий! Он предоставляет очень удобный способ запуска и CLEANLY разрушения док-контейнеров в тестах JUnit. Эта функция очень полезна для тестирования интеграции приложений с реальными базами данных и любыми другими ресурсами, для которых доступен образ докера.

Моя цель — продемонстрировать пример теста для Spring Boot Application на основе JPA с использованием TestContainers. Образец основан на примере в репозитории github TestContainer .

Образец приложения

Приложение на основе Spring Boot является простым — это приложение на основе Spring Data JPA с веб-слоем, написанным с использованием Spring Web Flux . Весь образец доступен в моем репозитории github, и может быть проще просто следовать коду прямо там.

Сохраняемая сущность City выглядит следующим образом (используя Kotlin ):

01
02
03
04
05
06
07
08
09
10
11
12
13
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id
 
@Entity
data class City(
        @Id @GeneratedValue var id: Long? = null,
        val name: String,
        val country: String,
        val pop: Long
) {
    constructor() : this(id = null, name = "", country = "", pop = 0L)
}

Все, что требуется для обеспечения хранилища для управления этим объектом, — это следующий интерфейс, благодаря отличному проекту Spring Data JPA :

1
2
3
4
import org.springframework.data.jpa.repository.JpaRepository
import samples.geo.domain.City
 
interface CityRepo: JpaRepository<City, Long>

Я не буду освещать здесь веб-слой, поскольку он не имеет отношения к обсуждению.

Тестирование репозитория

Spring Boot предоставляет функцию, называемую « Тесты срезов», которая представляет собой удобный способ тестирования различных горизонтальных срезов приложения. Тест для репозитория CityRepo выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.junit4.SpringRunner;
import samples.geo.domain.City;
import samples.geo.repo.CityRepo;
 
import static org.assertj.core.api.Assertions.assertThat;
 
@RunWith(SpringRunner.class)
@DataJpaTest
public class CitiesWithEmbeddedDbTest {
 
    @Autowired
    private CityRepo cityRepo;
 
    @Test
    public void testWithDb() {
        City city1 = cityRepo.save(new City(null, "city1", "USA", 20000L));
        City city2 = cityRepo.save(new City(null, "city2", "USA", 40000L));
 
        assertThat(city1)
                .matches(c -> c.getId() != null && c.getName() == "city1" && c.getPop() == 20000L);
 
        assertThat(city2)
                .matches(c -> c.getId() != null && c.getName() == "city2" && c.getPop() == 40000L);
 
        assertThat(cityRepo.findAll()).containsExactly(city1, city2);
    }
 
}

Аннотация «@DataJpaTest» запускает встроенные базы данных h2, настраивает JPA и загружает любые репозитории Spring Data JPA (в данном случае CityRepo).

Этот вид теста работает хорошо, учитывая, что JPA обеспечивает абстракцию базы данных, и если JPA используется правильно, код должен переноситься между любыми поддерживаемыми базами данных. Однако, если предположить, что это приложение будет работать на PostgreSQL в рабочей среде, в идеале должен быть какой-то уровень интеграционного тестирования с базой данных, в который входит TestContainer. Он обеспечивает способ загрузки PostgreSQL в качестве докера контейнер.

TestContainers

Тот же тест хранилища с использованием TestContainers выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.testcontainers.containers.PostgreSQLContainer;
import samples.geo.domain.City;
import samples.geo.repo.CityRepo;
 
import java.time.Duration;
 
import static org.assertj.core.api.Assertions.assertThat;
 
@RunWith(SpringRunner.class)
@DataJpaTest
@ContextConfiguration(initializers = {CitiesWithPostgresContainerTest.Initializer.class})
public class CitiesWithPostgresContainerTest {
 
    @ClassRule
    public static PostgreSQLContainer postgreSQLContainer =
            (PostgreSQLContainer) new PostgreSQLContainer("postgres:10.4")
                    .withDatabaseName("sampledb")
                    .withUsername("sampleuser")
                    .withPassword("samplepwd")
                    .withStartupTimeout(Duration.ofSeconds(600));
 
    @Autowired
    private CityRepo cityRepo;
 
    @Test
    public void testWithDb() {
        City city1 = cityRepo.save(new City(null, "city1", "USA", 20000L));
        City city2 = cityRepo.save(new City(null, "city2", "USA", 40000L));
 
        assertThat(city1)
                .matches(c -> c.getId() != null && c.getName() == "city1" && c.getPop() == 20000L);
 
        assertThat(city2)
                .matches(c -> c.getId() != null && c.getName() == "city2" && c.getPop() == 40000L);
 
        assertThat(cityRepo.findAll()).containsExactly(city1, city2);
    }
 
    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());
        }
    }
}

Ядро кода выглядит так же, как и в предыдущем тесте, но репозиторий здесь тестируется на реальной базе данных PostgreSQL. Чтобы вдаваться в подробности —

Контейнер PostgreSQL запускается с использованием правила класса JUnit, которое запускается до запуска любого из тестов. Эта зависимость извлекается с использованием зависимости gradle следующего типа:

1
testCompile("org.testcontainers:postgresql:1.7.3")

Правило класса запускает Docker-контейнер PostgreSQL (postgres: 10.4) и настраивает базу данных и учетные данные для базы данных. Теперь, с точки зрения Spring Boot, эти детали необходимо передать приложению, поскольку свойства ДО того, как Spring начинает создавать контекст теста для запуска теста, и это делается для теста с использованием ApplicationContextInitializer , который вызывается Spring очень рано жизненный цикл Spring Context.

Пользовательский ApplicationContextInitializer, который устанавливает имя базы данных, URL и учетные данные пользователя, подключается к тесту с помощью этого кода:

01
02
03
04
05
06
07
08
09
10
...
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
...
 
@RunWith(SpringRunner.class)
@DataJpaTest
@ContextConfiguration(initializers = {CitiesWithPostgresContainerTest.Initializer.class})
public class CitiesWithPostgresContainerTest {
...

При установленной плите котла TestContainer и тестирование среза пружинной загрузки возьмет на себя выполнение теста. Что еще более важно TestContainers также заботится о демонтаже, правило класса JUnit гарантирует, что после завершения теста контейнеры останавливаются и удаляются.

Вывод

Это был вихревой тур по TestContainers, TestContainers — это гораздо больше, чем то, что я здесь рассмотрел, но я надеюсь, что это даст представление о том, что возможно с помощью этой превосходной библиотеки и как ее настроить с помощью Spring Boot. Этот образец доступен в моем репозитории github

Смотрите оригинальную статью здесь: TestContainers и Spring Boot

Мнения, высказанные участниками Java Code Geeks, являются их собственными.