Рубрики
Без рубрики

Тестирование пружинной загрузки — Тестовые контейнеры и пролет

Это вторая часть серии статей о тестировании Spring Boot. Фрагменты кода взяты из… Помеченный как java, тестирование, docker, sql.

Это вторая часть серии статей о тестировании Spring Boot. Фрагменты кода взяты из этого репозитория . Вы можете клонировать его и запускать тесты, чтобы увидеть, как он работает.

Предыдущая глава. Тестирование Spring Boot – Данные и сервисы

Итак, мы узнали, как протестировать сервисный уровень и слой репозитория с базой данных H2. Такие тесты имеют некоторые преимущества. Например, они довольно быстры и не требуют сложной настройки ни на локальном компьютере, ни в среде CI/CD. Однако H2 – это не та база данных, которая обычно выполняется в рабочей среде. Если мы используем некоторые специфичные для поставщика функции одной базы данных, H2 может нам не помочь. Итак, сегодня мы обсудим, как запускать тестовые примеры на сервере true DB.

Автоматическое Создание Схемы

Как мы можем запускать тесты с реальной базой данных? Что ж, мы могли бы создать новый экземпляр локально и настроить тестовую среду, отредактировав src/test/resources/application.yml . Это действительно работает, но из-за этого сборку трудно воспроизвести. Каждый разработчик, работающий над проектом, должен быть уверен, что у него есть две отдельные базы данных. Один для разработки, а другой для запуска тестов. Кроме того, это делает выполнение сборки в среде CI/CD реальной проблемой.

Итак, Тестовые контейнеры на помощь! Это библиотека Java, которая запускает службу в контейнере Docker , запускает тесты и в конечном итоге уничтожает контейнер. Вам не нужно ни о чем беспокоиться, фреймворк выполняет свою работу. Просто убедитесь, что у вас установлен Docker, и тогда вы готовы к работе. Библиотека поддерживает десятки различных баз данных и модулей ( PostgreSQL , MySQL , MongoDB , Кафка , Elasticsearch , Nginx, Foxyproxy и многие другие). Даже если вы не нашли тот, который вам нужен, вы можете использовать generic container creation .

Первый шаг – добавить необходимые зависимости.

testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:postgresql'
runtimeOnly 'org.postgresql:postgresql'

Затем нам нужно создать отдельный конфигурационный файл в src/test/resources . Spring Boot способен различать различные конфигурационные файлы по profiles . Имя профиля должно быть помещено в виде суффикса типа application-PROFILE_NAME.yml . Например, файл конфигурации с именем application-test-containers.yml применяется только тогда, когда активен профиль test-containers .

spring:
  datasource:
    url: jdbc:tc:postgresql:9.6.8:///test_database
    username: user
    password: password
  jpa:
    hibernate:
      ddl-auto: create

Вы заметили, что tc суффикс в строке JDBC-подключения? Это волшебство, которое приходит с объединением JUnit 5 и тестовых контейнеров. Дело в том, что вам вообще не нужны никакие программные конфигурации! Когда фреймворк видит, что url содержит суффикс tc , он запускает все необходимые команды Docker внутри. Вы можете найти больше примеров здесь .

Мы устанавливаем spring.jpa.hibernate.ddl-auto=создать свойство таким образом, схема базы данных должна создаваться автоматически в соответствии с определением классов сущностей. Интеграция Flyway описана в следующем разделе.

Теперь давайте взглянем на метод Person Create Service.create Family и его повторный тест H2.

@Service
@RequiredArgsConstructor
public class PersonCreateServiceImpl implements PersonCreateService {

  private final PersonValidateService personValidateService;
  private final PersonRepository personRepository;

  @Override
  @Transactional
  public List createFamily(Iterable firstNames, String lastName) {
    final var people = new ArrayList();
    firstNames.forEach(firstName -> people.add(createPerson(firstName, lastName)));
    return people;
  }

  @Override
  @Transactional
  public PersonDTO createPerson(String firstName, String lastName) {
    personValidateService.checkUserCreation(firstName, lastName);
    final var createdPerson = personRepository.saveAndFlush(
        new Person()
            .setFirstName(firstName)
            .setLastName(lastName)
    );
    return DTOConverters.toPersonDTO(createdPerson);
  }
}
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@AutoConfigureTestDatabase
class PersonCreateServiceImplSpringBootTest {

  @Autowired
  private PersonRepository personRepository;
  @MockBean
  private PersonValidateService personValidateService;
  @Autowired
  private PersonCreateService personCreateService;

  @BeforeEach
  void init() {
    personRepository.deleteAll();
  }

  @Test
  void shouldCreateOnePerson() {
    final var people = personCreateService.createFamily(
        List.of("Simon"),
        "Kirekov"
    );
    assertEquals(1, people.size());
    final var person = people.get(0);
    assertEquals("Simon", person.getFirstName());
    assertEquals("Kirekov", person.getLastName());
    assertTrue(person.getDateCreated().isBefore(ZonedDateTime.now()));
  }

  @Test
  void shouldRollbackIfAnyUserIsNotValidated() {
    doThrow(new ValidationFailedException(""))
        .when(personValidateService)
        .checkUserCreation("John", "Brown");
    assertThrows(ValidationFailedException.class, () -> personCreateService.createFamily(
        List.of("Matilda", "Vasya", "John"),
        "Brown"
    ));
    assertEquals(0, personRepository.count());
  }
}

Как нам запустить тест с помощью тестовых контейнеров экземпляра PostgreSQL? Все, что нам нужно сделать, это добавить две аннотации. @База данных AutoConfigureTestDatabase хотя должен быть удален.

  1. @ActiveProfiles("тестовые контейнеры") — активирует профиль тестовые контейнеры таким образом, Spring мог прочитать файл конфигурации, который был описан ранее
  2. @Testcontainers — указывает на автоматический запуск экземпляра PostgreSQL в Docker
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@Testcontainers
@ActiveProfiles("test-containers")
class PersonCreateServiceImplTestContainers {

  @Autowired
  private PersonRepository personRepository;
  @MockBean
  private PersonValidateService personValidateService;
  @Autowired
  private PersonCreateService personCreateService;

  @BeforeEach
  void init() {
    personRepository.deleteAll();
  }

  @Test
  void shouldCreateOnePerson() {
    final var people = personCreateService.createFamily(
        List.of("Simon"),
        "Kirekov"
    );
    assertEquals(1, people.size());
    final var person = people.get(0);
    assertEquals("Simon", person.getFirstName());
    assertEquals("Kirekov", person.getLastName());
    assertTrue(person.getDateCreated().isBefore(ZonedDateTime.now()));
  }

  @Test
  void shouldRollbackIfAnyUserIsNotValidated() {
    doThrow(new ValidationFailedException(""))
        .when(personValidateService)
        .checkUserCreation("John", "Brown");
    assertThrows(ValidationFailedException.class, () -> personCreateService.createFamily(
        List.of("Matilda", "Vasya", "John"),
        "Brown"
    ));
    assertEquals(0, personRepository.count());
  }
}

Видишь? Проще простого!

Тесты репозитория

Как насчет тестирования слоев репозитория? Давайте еще раз посмотрим на пример H2.

@DataJpaTest
class PersonRepositoryDataJpaTest {

  @Autowired
  private PersonRepository personRepository;

  @Test
  void shouldReturnAlLastNames() {
    personRepository.saveAndFlush(new Person().setFirstName("John").setLastName("Brown"));
    personRepository.saveAndFlush(new Person().setFirstName("Kyle").setLastName("Green"));
    personRepository.saveAndFlush(new Person().setFirstName("Paul").setLastName("Brown"));

    assertEquals(Set.of("Brown", "Green"), personRepository.findAllLastNames());
  }
}

Правила те же, но есть небольшая разница. @DataJpaTest аннотируется с помощью @AutoConfigureTestDatabase сам по себе. Эта аннотация по умолчанию заменяет любой источник данных экземпляром H2. Итак, нам нужно переопределить это поведение, добавив replace= Заменять. НЕТ свойство.

@DataJpaTest
@Testcontainers
@ActiveProfiles("test-containers")
@AutoConfigureTestDatabase(replace = Replace.NONE)
class PersonRepositoryTestContainers {

  @Autowired
  private PersonRepository personRepository;

  @Test
  void shouldReturnAlLastNames() {
    personRepository.saveAndFlush(new Person().setFirstName("John").setLastName("Brown"));
    personRepository.saveAndFlush(new Person().setFirstName("Kyle").setLastName("Green"));
    personRepository.saveAndFlush(new Person().setFirstName("Paul").setLastName("Brown"));

    assertEquals(Set.of("Brown", "Green"), personRepository.findAllLastNames());
  }
}

Все по-прежнему работает нормально.

Интеграция пролета

Принцип эволюционного проектирования баз данных был описан давным-давно. В настоящее время использование инструментов, реализующих этот шаблон, является стандартной процедурой. Пролетная дорожка и Liquibase являются самыми популярными в мире Java. Мы собираемся интегрировать тестовые контейнеры с Flyway.

Во-первых, требуется зависимость от пролета.

implementation "org.flywaydb:flyway-core"

Во-вторых, необходимо отключить Flyway в application-test-containers.yml потому что там будет отдельный конфигурационный файл.

spring:
  datasource:
    url: jdbc:tc:postgresql:9.6.8:///test_database
    username: user
    password: password
  jpa:
    hibernate:
      ddl-auto: create
  flyway:
    enabled: false

Затем мы собираемся создать application-test-containers-flyway.yml . Библиотека предоставляет множество возможностей автоматической настройки. Так что, на самом деле, нам не нужно ничего настраивать.

spring:
  datasource:
    url: jdbc:tc:postgresql:9.6.8:///test_database
    username: user
    password: password

Теперь пришло время добавить миграции SQL. Каталог по умолчанию – resources/db/migration .

create table person
(
    id           serial primary key,
    first_name   text,
    last_name    text,
    date_created timestamp with time zone
);

Наконец, нам нужно заменить test-containers profile на test-containers-flyway one.

@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@Testcontainers
@ActiveProfiles("test-containers-flyway")
class PersonCreateServiceImplTestContainersFlyway {

  @Autowired
  private PersonRepository personRepository;
  @MockBean
  private PersonValidateService personValidateService;
  @Autowired
  private PersonCreateService personCreateService;

  @BeforeEach
  void init() {
    personRepository.deleteAll();
  }

  @Test
  void shouldCreateOnePerson() {
    final var people = personCreateService.createFamily(
        List.of("Simon"),
        "Kirekov"
    );
    assertEquals(1, people.size());
    final var person = people.get(0);
    assertEquals("Simon", person.getFirstName());
    assertEquals("Kirekov", person.getLastName());
    assertTrue(person.getDateCreated().isBefore(ZonedDateTime.now()));
  }

  @Test
  void shouldRollbackIfAnyUserIsNotValidated() {
    doThrow(new ValidationFailedException(""))
        .when(personValidateService)
        .checkUserCreation("John", "Brown");
    assertThrows(ValidationFailedException.class, () -> personCreateService.createFamily(
        List.of("Matilda", "Vasya", "John"),
        "Brown"
    ));
    assertEquals(0, personRepository.count());
  }
}

Запущенные тесты CI/CD

Хотя цель Testcontainers – упростить выполнение тестов, в сборке среды CI/CD есть некоторые предостережения. Некоторые поставщики интегрируются с тестовыми контейнерами прозрачно. Например, Travis-CI определяет требование автоматического запуска контейнера и выполняет эту работу внутри. Но некоторые другие инструменты могут потребовать дополнительной настройки — например, Дженкинс.

Червоточина докера

Это распространенный подход к упаковке всей сборки приложения в образ Docker во время запуска CI. Тестовые контейнеры могут обрабатывать это и запускать определенные контейнеры на главном сервере Docker. В этом случае нам нужно привязать /var/run/docker.носок как объем. Более того, каталог внутри контейнера должен совпадать с тем, в котором был запущен контейнер.

Например, это гипотетическая команда для запуска тестов в среде CI с помощью gradle .

docker run -it   \
           --rm  \
           -v $PWD:$PWD \
           -w $PWD  \
           -v /var/run/docker.sock:/var/run/docker.sock gradle:7.1-jdk8 \
           gradle test

Вы можете найти более подробную информацию на официальной странице Руководство по тестовым контейнерам .

Вывод

Сегодня мы обсудили, как протестировать уровень данных и сервисов с помощью тестовых контейнеров и Flyway. В следующий раз мы протестируем уровень API (контроллеров). Если у вас есть какие-либо вопросы или предложения, пожалуйста, оставьте свои комментарии ниже. Спасибо за чтение!

Оригинал: “https://dev.to/kirekov/spring-boot-testing-testcontainers-and-flyway-2jpd”