Продолжаем автоматизировать функциональные тесты на Kotlin и знакомиться с возможностями фреймворка Kotest Расскажу про расширения Kotest: Все части руководства: Максим Кочетков, QA Лид Автоматизации на одном из масштабных проектов Мир Plat.Form (НСПК). Проект транспортной процессинговой платформы зародился 3 года назад и вырос до четырех команд, где трудятся в общей сложности более 10 разработчиков в тестировании (SDET), а еще аналитики, разработчики и технологи. Наша задача — автоматизировать функциональные тесты на уровне отдельных сервисов, интеграций между ними и E2E до попадания функционала в релиз — всего порядка 40 микро-сервисов. От 1 до 5 микро-релизов в неделю. Взаимодействие между сервисами — Kafka, внешний API — REST, а также 3 фронтовых Web приложения. Разработка самой системы и тестов ведется на языке Kotlin, а движок для тестов был выбран Kotest. В данной статье и в остальных публикациях серии я максимально подробно рассказываю о тестовом Движке и вспомогательных технологиях в формате Руководства/Tutorial. Что такое расширение для фреймворка тестирования? Это класс, который реализует определенный интерфейс-маркер для фреймворка тестирования или его производные интерфейсы. Интерфейс расширения предоставляет методы по перехвату событий жизненного цикла или даже для изменения этого цикла. Например, интерфейс расширения, которое может отключить тест, то есть изменить его жизненный цикл. Расширение подключается к проекту / классу / тесту с помощью аннотаций или программно: добавлением в реестр расширений. Например, расширения Junit5 обычно подключаются на класс с помощью аннотации @ExtendWith: Также этой аннотацией можно пометить другие аннотации: Система расширений — это стандартный функционал для современного тестового фреймворка. Я бы выделил два вида расширений в Kotest: Listener и Interceptor Например, недавно появившийся в Kotest интерфейс InstantiationErrorListener позволяет перехватить ошибку при создании сущности класса теста. Он решает проблему, когда в результате неверного контекста или ошибок в инициализации класс с тестами просто не удалось создать. Тогда в отчете может вообще отсутствовать этот несозданный тест, отчет будет успешным, а сборка проваленной. Все слушатели могут выполняться асинхронно, так как не влияют на жизненный цикл теста и друг друга, а результат их выполнений собирается в коллекцию. Первое перехватываемое событие перед началом выполнения теста реализуется в слушателях
BeforeInvocationListener:
 В качестве примера приведу фундаментальное расширение: ConstructorExtension.
Оно перехватывает момент создания объекта класса теста, на выходе ожидает внутренний объект фреймворка — Spec.  Тут можно также обработать ошибки создания, как в InstantiationErrorListener, но также придется взять на себя ответственность за подготовку всего тестового класса для дальнейших действий в цепочке. На самом деле внутри Kotest большая часть функционала также запускается по модели перехватчиков. Специальным внутренним перехватчикам делегируется поиск и запуск пользовательских расширений:  В ходе этого этапа создается функция высшего порядка с помощью свертывания fold, с порядком, равным количеству перехватчиков. То есть каждый перехватчик превращается в функцию, которая вызывается внутри другого перехватчика, и результат последнего выполнения рекурсивно передается в вышестоящую функцию. Тот же Spring Test можно нормально использовать совместно с тестовым Фреймворком только через расширение, так как необходимо контролировать создание сущностей тестов и контекст к ним. Расширения для всех основных фреймворков тестирования работают примерно одинаково, поэтому на единой кодовой базе можно очень быстро создать набор адаптеров-расширений для всех фреймворков, а все вызовы делегировать единой внутренней логике. Очень популярны расширения для создания тестовых дублёров (test doubles). Например, с помощью расширения MockitoExtension для Junit5 можно легко создать заглушки всех репозиториев и не поднимать реальную базу, просто добавив аннотацию @Mock на поле. Вся документация по расширениям есть в разделе Extensions. Но мы здесь собрались, чтобы попробовать самое интересное, а не парсить документацию. В Kotest есть набор встроенных расширений. Все они в артефакте io.kotest:kotest-extensions-jvm,
который транзитивно приходит вместе с основным io.kotest:kotest-runner-junit5. Находятся в пакете io.kotest.extensions — просто имейте это ввиду, там их много, а я расскажу про несколько. Все просто — в конструкторе SystemEnvironmentTestListener мы задаем набор переменных окружения, которые подменяем на время работы теста. Внутри теста эти переменные будут иметь значения, указанные пользователем. После теста переменные возвращают свои значения. Расширение не потокобезопасно, поэтому нужно помечать тест @DoNotParallelize — даже если не пускаете тесты параллельно, нужно пометить! Либо применять расширение сразу ко всему проекту. Иногда в тестируемом приложении может вызываться System.exit(), чтобы завершить процесс, например, выполнить graceful shutdown или завершить проложение из-за недостатка ОП с кодом 137. Эту ситуацию можно перехватить и проверить, что: Часто в приложении используется текущая дата, обычно в UTC. И часто очень сложно предсказать какая получится текущая дата, чтобы ее проверить на выходе. Также часто необходимо подменить текущую дату на определенную, чтобы проверить логику и не очень удобно делать это через системное время. Рассматриваемое расширение решает эту проблему и позволяет переопределить метод now() у любой реализации Temporal, например LocalDate / ZonedDateTime. Вот мы и добрались до кульминации рассказа. На самом деле все это затевалось, чтобы показать как Kotest работает со Spring Test и Spring Boot Test в частности. Что использовать Kotest для написания unit / интеграционных / e2e / функциональных тестов для вашего Spring Boot приложения не сложнее, чем Junit. А в части читаемости и поддерживаемости функциональных тестов даже предпочтительнее, по мнению автора. Начинаем с плагинов: Плагин для поддержки kotlin, плагин для корректной работы Spring AOP с неизменяемыми по умолчанию классами котлина kotlin.plugin.spring и spring boot плагин. Далее активируем удобный spring.dependency-management из состава spring boot плагина, чтобы использовать заранее подготовленный BOM с версиями большинства библиотек
и не заботится о совместимости. После подключения нет необходимости указывать версии библиотек в блоке dependencies. Версии можно проверить на сайте docs.spring.io Подключаем зависимости: Включаем junit5 — он в любом случае нужен как для запуска собственных тестов, так и для запуска тестов kotest: Добавляем объект SpringExtension для всего проекта. То есть контекст будет подниматься и переиспользоваться для любой Kotest спецификации.
Если хочется включить Spring только для некоторых классов, то добавляем расширения на уровне спецификации: Так или иначе, после включения расширения создается спринговый TestContextManager, которому делегируется инициализация контекста и класса спецификации. По умолчанию подключается возможность внедрять все аргументы из конструктора теста, даже без дополнительных аннотаций типа @Autowired. Эта возможность доступна через Kotest перехватчик ConstructorExtension — Spring расширение реализует его и берет на себя создание объекта класса спецификации. Тут аргумент propertyResolverInConstructorвнедряется без дополнительных телодвижений, как полагается, через конструктор, а не через setter! Тестируем Spring контроллер с одним методом POST, который проверяет текст в запросе. Входящий запрос: Ответ: И сам контроллер: В тесте мы поднимаем полноценный сервер на случайном порту, подключаемся к нему HTTP клиентом, отправляем реальные запросы и проверяем реальные ответы, — поэтому End 2 End. Будем писать, как полагается, Data Driven Test на 3 набора входных данных в BDD стиле. Теоретически такой тест может быть использован в BDD подходе к разработке. И написание этого теста не требует наличия рабочей функциональности — только четкие требования. Для сценария Kotest предоставляет множество стилей — я всегда выбираю FreeSpec. Во всех предыдущих частях использовал либо StringSpec для плоских тестов без вложенности, либо FreeSpec для тестов с вложенностью и логическими блоками. Я хочу показать, что в конструкторе можно внедрить любой Bean из контекста без использования аннотаций: В блоке init оформляю структуру сценария прямо как в Cucumber без реализации, которую уже можно запустить и отдать на ревью коллегам: Добавляем реализацию шагов: Отлично тест готов! Если запустить его в Idea, то вывод хода выполнения будет таким:  Контекст стартует один раз на все тестовые классы и переиспользуется, если нет аннотации @DirtiesContext. У меня старт данного приложения занимает примерно 2.297 секунды В одном проекте и даже в одной source-папке успешно уживаются разные тестовые движки, интегрируемые через JunitPlatform. Старый добрый Junit5 успешно справляется с E2E DataDriven тестом нашего Spring Boot приложения. А вот какой вывод имеем в окне с ходом выполнения теста:  Тест JUnit5 выглядит компактнее, так почему же мы используем Kotest для функционального тестирования.  Подчеркну, что именно для него — в модульных тестах успешно работает JUnit5.  А в функциональных тестах нам важна очень четкая и гибкая структура сценария и тестовых данных, разбитая на логические блоки — Kotest подходит идеально в нашем тех. стеке. Естественно для успешного анализа результатов сложной логики функциональных тестов необходима отчетность.  И чтобы вся структура теста и тестовых данных корректно легла в эту отчетность... Добавляем Allure отчеты в тесты и заодно в RestAssured, чтобы подхватить отправленные запросы и принятые ответы.  Само собой, для добавления используем расширение. Документация рекомендует io.kotest.extensions:kotest-extensions-allure, но оно нам к сожалению не подходит по нескольким причинам: Но есть решение!  Альтернативное расширение ru.iopump.kotest:kotest-allure — оно работает и все корректно отображает, а также отмечено звездочкой от создателя Kotest, поэтому можно доверять. Подключаем расширение в Gradle, а заодно и добавим и для RestAssured Далее в коде… А в коде для Kotest добавлять не нужно, так как расширение помечено аннотацией @AutoScan и подключается автоматически. Тогда для RestAssured — сделаем это на уровне проекта: И набросаем минимальный набор аннотаций, который мы обязательно добавляем на каждый тест-класс в Mir.Platform Чтобы ссылки на задачи корректно работали, необходимо оформить шаблоны для ссылок в allure.properties и положит в папку resources/. Вот пример, который будет подставлять вместо {} номер задачи и вести в youtrack.jetbrains.com: Сюда можно добавлять свои шаблоны, давать им имена и создавать собственные аннотации для определенного вида ссылок. Для добавления задач генерации отчета и включения перехвата Allure аннотаций через AspectJ желательно подключить Gradle-плагин: У нас используется несколько Фреймворков для тестирования, а также Kotest пока не поддерживается Allure плагином, то полностью автоматическая конфигурация нам не подойдет: Запускаем тесты и генерацию Allure-отчета: И остается разобрать сгенерированный Allure отчет, хотя там все довольно очевидно
 По традиции, ссылка на все примеры qa-kotest-articles/kotest-third. Наконец-таки закончился цикл статей про Kotest — мы разобрали все основные аспекты данного фреймворка и было продемонстрировано успешное применение для тестирования Spring приложения на любом уровне.  С помощью системы расширений можно добавить любую, не упомянутую мной функциональность в Kotest: ⚡ Блог Мир Plat.Form ⚡ Kotlin. Автоматизация тестирования (часть 1). Kotest: Начало ⚡ Kotlin. Автоматизация тестирования (Часть 2). Kotest. Deep Diving ⚡ Примеры кода ⚡ Kotest GitHub ⚡ Kotest Spring GitHub ⚡ Kotest Allure GitHub 📔 Официальная документация Kotest 📔 Kotlin Lang 📔 JUnit5 📔 Gradle Testing Automation QA