Дорогой читатель, это мой первый туториал и если я что‑то упустил или не объяснил, хотя стоило бы, напиши пожалуйста комментарий и я обновлю статью.

TL;DR

Весь код для микросервисов и e2e теста тут

Рекомендую быстро пробежаться по коду перед прочтением статьи т к в самой статье я объясню только неочевидные моменты реализации e2e тестирования.

Проблема

Нужно организовать сквозное/e2e (end-to-end) тестирование (далее буду использовать термин e2e) приложения, которое состоит из нескольких микросервисов, микросервисы используют сторонние апи.

Знакомимся c проектом

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

Аккаунт можно создать только когда тебе есть 18 лет.

А в постах нельзя использовать матные слова. Если найдено матное слово — пост не будет создан.

Разные детали про проект

  • в проекте используется монорепозиторий

  • технологии: maven, Spring Boot, Hibernate, H2, Feign client (из Spring Cloud)

  • в целях упрощения туториала не используем никаких либ для миграции базы (liquibase, flyway) и создаем таблицы с помощью Hibernate

  • в целях упрощения в качестве базы используем in-memory базу H2

  • для примера используется приложение с двуми микросервисами, но их может быть и больше

  • e2e тесты лежат в отдельном модуле с названием e2e-tests

  • для запуска сервисов для e2e тестирования будем использовать Spring профиль с название ‘e2e’

Про сервисы и их обязанности

У нашей социальной сети есть два микросервиса и эти микросервисы пользуются сторонними апи. Ниже диаграмма с описанием приложения.

UserService — отвечает за все, что связано с пользователем.
PostService — отвечает за все, что связано с постами.
Age API — сторонний сервис, который помогает нашей соцсети узнать реальный возраст пользователя.
Word API — сервис, который помогает понять есть ли в тексте матные слова.

Краткое описание технологий для e2e тестирования

Для создания e2e тестов будут использованы следующие либы: JUnit 5, Wiremock 2, Cucumber 7, Awaitility 4.

Wiremock — для мокирования(имитации) http ответов. В нашем случае используем wiremock для мокирования ответов от сторонних API. Короткий пример (создаем мок для get запроса на получение пользователя с id 1):

wiremockServer.stubFor(get("/api/users/1").willReturn(aResponse()
        .withStatus(200)
        .withHeader("Content-Type", "application/json")
        .withBody(jsonHelper.fromObjectToString(user)))
 );

Cucumber — для создания удобного для чтения e2e теста. Короткий пример:

Функция: Снятие денег со счета
    Сценарий: Успешное снятие денег со счета
    Дано на счете пользователя имеется 1200 рублей
    Когда пользователь снимает со счета 200 рублей
    Тогда на счете пользователя имеется 1000 рублей

Awaitility — для создания проверок в нашем e2e тесте с условиями, которые могут выполниться не сразу, а через какое-то время. Короткий пример:

// сделали что-то с кастомером
someCustomerService.doSomething();

// тут мы ждем 5 секунд и проверяем обновился ли статус кастомера
await().atMost(5, SECONDS).until(customerStatusIsUpdated());


// или могли бы проверять обновился ли статус кастомера каждые
// 2 секунды в течение 10 секунд
Awaitility.await().atMost(10, TimeUnit.SECONDS)
                .pollInterval(2, TimeUnit.SECONDS)
                .until(customerStatusIsUpdated());

Проговариваем логику e2e теста

Цель протестировать весь жизненный цикл пользователя в нашей социальной сети.

Напомню, что в соцсети нельзя создавать акки, если тебе меньше 18 лет и нельзя использовать маты в постах.

Логика теста: пользователь создает аккаунт в соцсети (попробуем создать аккаунт для двух людей: одному есть 18 лет, другому нет) и ведет какую-то активность (создадим пару постов с матами и без) и в итоге решает удалить свой аккаунт.

Что нужно сделать для успешного выполнения e2e теста

Чтобы провести e2e тестирование нам нужно замокать все сторонние сервисы, в этом нам поможет Wiremock. Наши сервисы будут работать и ничего не подозревать о том, что сторонние API не настоящие. На диаграмме ниже можно увидеть, что сервисы соцсети обращаются к wiremock, а не к реальным апи.

Подготавливаем сервисы для e2e тестирования

Для ускорения выполнения e2e тестов можно использовать in-memory базы данных. В нашем примере мы так и делаем — используем H2 базу данных вместо, например, MySql.

Так же нужно чтобы http клиенты, которые используются для общения со сторонними API, можно было перенаправить на Wiremock (то есть нужно изменить хост и порт для сторонних API). При этом хотелось бы чтобы использовался тот же самый клиент что и для прода. В нашем примере это сделано через спринговые проперти.

В проекте нашей социальной сети для создания клиентов используется FeignClient (из Spring Cloud), но и для других либ для создания клиентов логика будет такая же.

Клиент:

Проперти в аппликейшен файле для e2e профиля с указанием хоста и порта Wiremock-a:

Читаем готовый e2e тест и разбираемся как писать свои

Наша цель создать легкочитаемый e2e тесты. Для этого мы используем либу Cucumber. А читабельное описание нашего теста будет храниться в файле с расширением .feature

Ниже готовый файл с шагами для нашего e2e теста:

Наш файл .feature содержит ключевые слова:
“Функция” — тут обычно пишут описание тестируемого функционала. В нашем случае у нас один тест, который тестит вообще всё приложение, поэтому я использовал подходящее описание.
“Сценарий” — название и краткое описание нашего e2e теста.
“Дано”, “Когда”, “Тогда” — это ключевые слова, которые мы используем для описания шагов теста.
Так же доступны такие ключевые слова: Допустим, Если, Затем, И, Иначе, Ктомуже, Но, Пусть, Также, То.

Напомню, что все эти ключевые слова это синтаксический сахар.

Плагин для файлов .feature

Для удобного редактирования файлов типа .feature нужно установить плагин. IntelliJ Idea подскажет установить его когда вы откроете файл типа .feature

Пишем в фича файле на русском языке

Название шагов на русском можно писать без каких-либо дополнительных настроек. А чтобы можно было использовать русские ключевые слова нужно в начале .feature файла добавить это:

# language: ru

Пример можно увидеть на скриншоте выше.

Создаем новый шаг в фича файле

Шаг, для которого еще нет кода, который будет выполняться, подсвечивается желтым цветом:

Если навести курсов на этот шаг или прожать Alt+Enter когда курсор стоит на этой строке, то можно увидеть такое меню

Тут идея предлагает создать так называем step definition или метод, который будет содержать код, который будет выполнен для этого шага во время теста.

Если кликнуть на ”Create step definition”, то появится такое меню

Я выбрал существующий файл ThirdPartyServicesStepsDefinitions, если у вас таких пока нет, то выбирайте первый вариант.

Для меня в классе был создан метод с аннотацией @Дано, а также название метода на русском (если вдруг не знали, то можно писать на java используя русский язык ????). В этом методе нужно будет поместить код для этого шага

Давайте посмотрим на то, что я положил в этот метод. Чтобы произошло то, что написано в названии этого шага, в коде мы должны замокать ответ от Age Api. Вот как это сделано:

Тут создается мок для POST запроса по урлу /is-person-adult а также добавлено условие, что в теле запроса должен быть определенный джисон.

Проверяем с помощью Awaitility условие, которое выполнится не сразу

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

В нашем e2e тесте Awaitility используется на шаге проверки того, что у пользователя больше нет постов в соцсети. Тут нам нужен Awaitility т к в нашей соцсети удаление пользователя и его постов занимает много времени.

В этом методе мы запускаем проверку того, что у пользователя больше нету постов. Каждые 2 секунды на протяжении 10 секунд Awaitility будет выполнять код из метода .until()

Если в течение 10 секунд лямбда из .until() не вернет true, то вылетит эксепшен и тест завалится.

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

Смотрим вспомогательный код для e2e теста

Код для реализации логики в шагах из фича файла:

  • в пакете src/main/java/org/example можно увидеть класс c кодом для запуска Spring Boot приложения (в нем выключены сервлеты чтобы запускался только spring контекст). Этот класс нужен нам т к без него не получится запустить e2e тест со spring контекстом, и значит не получится удобно и красиво инжектировать бины в классы.

  • в пакете src/test/java/org/example/common/client у нас клиенты для сервисов для проведения CRUD операций для постов и пользователей.

  • src/test/java/org/example/common/config — пакет с конфигами, про них чуть ниже.

  • класс JsonHelper это обертка для ObjectMapper для работы с джисонами.

  • класс TestContext это класс для удобного хранения и передачи данных между шагами теста, который в сути своей обычная мапа.

  • класс AbstractStepsDefinitions — содержит код, который будет использован всеми остальными StepsDefinitions классами

Конфиги для запуска e2e теста

  • BeanConfig — тут ничего особенного, он содержит методы для создания spring бинов (в этом классе мы создаем инстанс Wiremock сервера)

  • CucumberSpringConfiguration — класс для конфигурации application context (в нашем случае это Spring контекст) который будет подниматься при запуске .feature файлов.

  • EndToEndCucumberTestConfiguration — в этом классе содержатся настройки для запуска .feature файлов с помощью JUnit 5. Давайте пройдемся по аннотациям из этого класса т к они могу ввести в замешательство.

@Suite — это аннотация из JUnit 5 для создания класса-запускатора тестов

@IncludeEngines("cucumber") — эта аннотация для добавления Cucumber engine (движка для запуска тестов созданных с использованием Cucumber).

@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "org.example") — тут мы указываем местоположение джава классов со StepsDefinitions и также в пакете должен быть класс с аннотацией @CucumberContextConfiguration иначе будет ошибка

@ConfigurationParameter(key = FEATURES_PROPERTY_NAME, value = "src/test/resources/features/") — тут указываем путь к пакету с .feature файлами

Запуск сервисов и e2e теста

Запускаем сервисы с e2e профилем. В корне модуля для PostService (/social-network-java-spring-wiremock-cucumber-e2e-test/post-service) запускаем:

mvn spring-boot:run '-Dspring-boot.run.profiles=e2e'

Таким же образом запускаем UserService.

Запускаем тесты из консоли. Переходим в модул с e2e тестами (/social-network-java-spring-wiremock-cucumber-e2e-test/e2e-tests) и запускаем:

mvn clean install

Результат:

Если у вас появились какие-то вопросы, пишите, отвечу или обновлю пост.

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


  1. Ahim
    00.00.0000 00:00
    +1

    • подход рабочий мы на своих проектах завернули в виде библиотечки для тестов с разными моками в дополнение к http

    • e2e тесты все же обычно используют реальное окружение, я бы как-нибудь по-другому назвал

    • рекомендую wiremock запускать на случайном порту чтобы не конфликтовал сам с собой/другими проектами например на ci, что-то типа

     wireMock = new WireMockServer(wireMockConfig().dynamicPort())
     ...
        public int getPort() {
            return wireMock.port();
        }
    # в конфиге
    yourUrl: http://localhost:${beanWithWiremock.getPort()}/
    ... 
    


    1. odisseylm
      00.00.0000 00:00
      +1

      Согласен с вами - это должно называться полу/аля integration unit tests)) И может использоваться как дополнение (или вместо) unit tests, но ни как e2e tests.

      Полноценные e2e tests должны использовать реальную базу, реальный docker image, только с и измененными адресами зависимостей, и image с моками.