Очень часто мы сталкиваемся с проблемой скорости выполнения тестов в pipeline или при обычном запуске с помощью Gradle или Maven локально. В данной статье мы с вами рассмотрим, как настроить наш Spring-контекст и Testcontainer так, чтобы сократить время инициализации окружения  (Spring Boot, базу данных) для корректной работы тестов и, соответственно, ускорить скорость прохождения компонентных тестов. Ранее мы уже говорили о тестировании приложений, но в прошлом материале было больше теории, чем практики. В этой статье мы расскажем о реальной задаче с проекта.

Автор материала: @vladgusev

Владимир Гусев

Java разработчик

Некоторые причины медленной работы тестов

При запуске компонентных тестов много времени занимает запуск окружения, а именно:

  • разворачивание БД;

  • выполнение DDL и DML скриптов из Liquibase (или Flyway);

  • создание всех компонентов из Spring-контекста.

И при неверной настройке тестов всё это может происходить несколько раз. Что нужно сделать, чтобы все сработало с 1 раза? Для этого нам нужно:

  1. Сделать чтобы все тесты использовали один и тот же образ БД.

  2. Сделать чтобы все тесты использовали один и тот Spring-контекст.

За основу возьмем наше приложение data-service, которое использует:

  • Spring Boot 3;

  • Liquibase;

  • PostgreSQL 15;

  • Junit 5.

В data-service уже присутствует 136 модульных тестов, из них 90 компонентных. Нам проще будет увидеть разницу во времени выполнения тестов на таком приложении, чем на приложении, в котором 1–2 теста.

Настройка Testcontainer для БД

Начнем с настройки нашего класса конфигурации для Testcontainer: 

@ExtendWith(SpringExtension.class)
@SpringBootTest
@TestcontainersTestcontainers
@DirtiesContext
@AutoConfigureMetrics
@MockBean(classes = {MetadataCache.class, MetadataFieldCache.class, MetadataCacheAndUiMetadataCacheService.class,
        MigrationCacheService.class})
public class ContainerEnvironmentTest {

    @Container
    static PostgreSQLContainer postgreSQLContainer = (PostgreSQLContainer) new PostgreSQLContainer("postgres:15")
            .withInitScript("sql/init.sql")
            .withDatabaseName("postgres")
            .withUsername("password")
            .withPassword("password");


    @DynamicPropertySource
    static void property(DynamicPropertyRegistry registry) {
        postgreSQLContainer.start();
        registry.add("spring.datasource.url", () ->      postgreSQLContainer.getJdbcUrl());
        registry.add("spring.datasource.username", () -> postgreSQLContainer.getUsername());
        registry.add("spring.datasource.password", () -> postgreSQLContainer.getPassword());

        registry.add("spring.liquibase.enabled", () -> true);
    }

    @AfterAll
    public static void afterAll() {
        postgreSQLContainer.stop();
    }

}

 Сделаем все классы тестов наследников от ContainerEnvironmentTest:

Объявление класса-теста.
Объявление класса-теста.

Давайте запустим и посмотрим на результат.

Результат выполнения тестов 1.
Результат выполнения тестов 1.

136 тестов отработало за 3 минуты и 48 секунд. От сложности и количества тестов будет расти и время их выполнения. Это пример настройки Testcontainer, с которой мы начали свой путь ускорения тестов.

Использование одного родителя для всех классов-тестов нам значительно увеличивает вероятность (но пока не гарантирует), что все тесты будут использовать один и тот же образ базы данных.

При таком способе конфигурации контейнера все тесты получают настройку образа из одного источника – из класса родителя. Осталось только сделать правильную конфигурацию Testcontainer.

Улучшение конфигурации Testcontainer

Такая скорость прохождения тестов нас не устроила. Сначала нужно понять, в чем может быть причина медленной скорости выполнения тестов.

Первое, что бросается в глаза это то, что контейнер PostgreSql создается несколько раз, и Liquibase скрипты выполняются, соответственно, каждый раз после создания контейнера.

Давайте попробуем изменить наш класс-контейнер так, чтобы база создавалась один раз.

@ExtendWith(SpringExtension.class)
@SpringBootTest
@TestcontainersTestcontainers
@DirtiesContext
@AutoConfigureMetrics
@MockBean(classes = {MetadataCache.class, MetadataFieldCache.class, MetadataCacheAndUiMetadataCacheService.class,
        MigrationCacheService.class})
public class ContainerEnvironmentTest {

    static final PostgreSQLContainer postgreSQLContainer;

    static {
        postgreSQLContainer = (PostgreSQLContainer) new PostgreSQLContainer("postgres:15")
                .withInitScript("sql/init.sql")
                .withDatabaseName("postgres")
                .withUsername("password")
                .withPassword("password")
                .withReuse(true);
        postgreSQLContainer.start();
    }

    @DynamicPropertySource
    static void property(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", () -> postgreSQLContainer.getJdbcUrl());
        registry.add("spring.datasource.username", () -> postgreSQLContainer.getUsername());
        registry.add("spring.datasource.password", () -> postgreSQLContainer.getPassword());
        registry.add("spring.liquibase.enabled", () -> true);
    }
}

Готово! Что изменилось:

  1. Мы стали использовать метод withReuse(true). Используя его, мы явно даем понять, что не обязательно убивать контейнер после выполнения всех тестов из класса. Тем самым мы оставим тот же контейнер для работы с тестами из других классов.  

  2. Мы убрали метод afterAll, который останавливал работу базы, так как нам нужно оставить текущий контейнер базы данных для других тестов.

Давайте запустим выполнение тестов еще раз и посмотрим на разницу.

Результат выполнения тестов 2.
Результат выполнения тестов 2.

Мы уменьшили время выполнения тестов. Теперь они выполняются за 3 минуты 12 секунд. Мы поправили только одну проблему — контейнер с БД поднимается один раз и соответственно Liquibase скрипты теперь отрабатывают один раз. При этом по логам можно заметить, то наш контекст постоянно поднимается с нуля — это значит, что тратится время на инициализацию нашего spring-контекста.

Как использовать один контекст во всех тестах?

В данном случае контекст создается несколько раз из-за использования аннотации @DirtiesContext.

Причиной такого поведения может быть и разный набор моков в классах-тестах. Если вам нужен один контекст для всех компонентных тестов, то в подобный класс-родитель можно добавить создание моков для клиентов сторонних сервисов (чаще всего — это Feign клиенты).

@DirtiesContext — это аннотация для тестирования Spring. Эта аннотация указывает на то, что связанный тест или класс изменяет ApplicationContext. Она сообщает среде тестирования, что нужно закрыть и воссоздать контекст для последующих тестов. Ее можно настроить как для класса, так и для метода.

Попробуем ее убрать и посмотреть на результат. И тут возникает неприятный сюрприз.

Сюрприз!
Сюрприз!

Тесты упали, хотя, казалось бы, не должны. Причиной стала проблема нехватки соединений к базе данных.  К сожалению, тесты не всегда успевают закрыть свои соединения к БД, из-за чего и происходит данный конфуз.

Эту проблему можно решить довольно просто и при старте контейнера установить базе больше соединений. По умолчанию их примерно 100 (точное число я не помню).

Для этого при создании контейнера укажем «max_connections=20 000» вот так (остальной код оставляем таким же):       

postgreSQLContainer.setCommand("postgres", "-c", "max_connections=20000");
postgreSQLContainer.start();

Давайте попробуем запустить.

Результат запуска тестов 3.
Результат запуска тестов 3.

Ура! Тесты отработали корректно, и что же мы видим? Отработали они за 2 минуты и 24 секунды. На еще большем объеме данных разница в производительности будет более заметной.

Также прикладываю скрины прогонки тестов в pipelines.

До:

После:

Итого

Мы ускорили работу тестов, немного изменив настройки контейнера и контекста. В данном случае мы сделали так, чтобы:

  1. Поднимался 1 экземпляр контейнера с базой данных на все тесты.

  2. Запускались скрипты Liquibase один раз (следует из пункта).

  3. Использовался один контекст для выполнения всех компонентных тестов.

Важно помнить, что при данных настройках нужно держать тестовую базу консистентном состоянии для тестов, иначе могут быть коллизии по данным.

При медленной работе тестов не стоит от них отказываться. Есть пути оптимизации и изложенный в этой статье подход не единственный, но достаточно распространенный. Например, можно сделать параллельный запуск тестов или вынести компонентные тесты в отдельный pipeline. Но мы рекомендуем начать именно с этого способа как с самого простого.

Комментарии (1)


  1. Bifurcated
    03.11.2024 22:10

    Зачем использовать аннотацию @ExtendWith(SpringExtension.class) если она и так есть в @SpringBootTest, так же видимо опечатка с аннотацией @TestcontainersTestcontainers . Помимо @DirtiesContext, чтобы контекст нигде дальше не перезапустился лучше сразу в базовом определить все @MockBean и @SpyBean. И покажи в конце что у вас в итоге получилось.