[Тестирование IT-систем, Тестирование веб-сервисов, Kotlin] Kotlin. Автоматизация тестирования (часть 1). Kotest: Начало
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Хочу поделиться опытом создания системы автоматизации функционального тестирования на языке на Kotlin.
Основой для создания / конфигурирования / запуска / контроля выполнения тестов — будет набирающий популярность молодой фреймворк Kotest (в прошлом Kotlin Test).
Проанализировав все популярные варианты для Kotlin — выяснилось, что есть всего два "нативных":
Либо бесконечное кол-во из Java мира: Junit4/5, TestNG, Cucumber JVM или другие BDD фреймворки.
Выбора пал на Kotest с бОльшим кол-вом "лайков" на GitHub, чем у Spek.
Руководств по автоматизации тестирования на Kotlin, особенно в сочетании с Kotest — немного (пока).
Думаю, что написать цикл статей про Kotest, а также про организацию проекта авто-тестов, сборку, запуск и сопутствующие технологии — хорошая идея.
По итогу должно получиться емкое руководство — как создать систему, или даже эко-систему, автоматизации функционального тестирования на языке Kotlin и платформе Kotest.
Минусы
Сходу определимся по глобальным минусам и учтем, что проект быстро развивается. Те проблемы, что есть в актуальной на время написания статьи версии 4.2.5, уже могут быть исправлены в свежих релизах.
Первым, очевидным, минусом является очень быстрое изменение минорных и мажорных версий.
Еще в марте 2020 фреймворк назывался KotlinTest, с версии 4.0.0 поменял название всех пакетов, потерял совместимость с плагином Idea и был переименован в Kotest, а также стал мульти-платформенным.
После обновления с версии 4.0.7 до 4.1 перестали работать расширения, написанные ранее, также были изменены названия базовых модулей и много чего еще, то есть принцип семантического версионирования нарушился между минорными версиями 4.0 и 4.1.
Это немыслимо для Java мира — это что-то из JS.
У инструмента пока небольшое комьюнити.
В пользу широкого функционала иногда приносится в жертву продуманный дизайн.
Не все предоставляемые стили написания тестов адекватно транслируются в отчет. На текущий момент не корректно отображаются репорты для data-driven и property-based тестов.
Шаблоны и фабрики тестов работают не как ожидается.
Встроенное расширение allure совсем никуда не годится (к примеру, оно пытается обрабатывать аннотации, которыми в принципе невозможно аннотировать тестовые DSL методы).
Однако ни одного критического или блокирующего дефекта я не встретил.
Почему Kotest?
Многие разработчики на Kotlin не заморачиваются с выбором тестового фреймворка и продолжают использовать Junit4 или Junit5.
Для них тесты — это, как правило, класс, помеченный аннотацией @SpringBootTest, набор методов с аннотацией @Test, возможно методы before и beforeClass с соответствующими аннотациями.
Для полноценных функциональных e2e тестов этого недостаточно.
Нужен инструмент предоставляющий возможность создавать понятные тесты на основе требований, удобную организацию проверок, тестовых данных и отчетности.
Так вот Kotest позволяет:
- писать крайне понятные тесты в BDD стиле с помощью Kotlin DSL и функций расширения,
- легко создавать data driven тесты в функциональном стиле
- с помощью DSL определять обратные вызовы перед тестом и тестовым классом и после них.
- определить действия на уровне всего прогона (фича, которой нет явно в junit)
- использовать встроенные интуитивные проверки
- простое конфигурирование тестовых классов и тестового проекта из кода
и много чего еще, см. полную документацию и сам проект в GitHub
Начинаем создавать тест
Какой стиль выбрать?
Kotest дает возможность выбора между несколькими вариантами DSL для формирования структуры тестов.
Самый базовый и простой String Spec — идеально подойдет для написания unit-тестов с одним уровнем вложенности.
Для полноценных функциональных авто-тестов нужно что-то посложнее: по структуре схожее с Gherkin, но менее формализованное, особенно по ключевым словам.
После долгих экспериментов я остановился на стиле FreeSpec.
Используя Kotest я рекомендую продолжать писать тесты в BDD стиле, как в языке Gherkin (Cucumber).
FreeStyle не накладывает ограничений на именование тестов, ключевые слова и вложенность, поэтому эти вещи нужно контролировать на уровне code-style, best practice, обучения и Merge-Request`ов.
Иерархия тестовых сущностей
В нашем подходе будет 5 базовых тестовых сущностей (или уровней) в рамках Kotest.
Важно определить это сейчас, потому что в дальнейшем оперировать я буду этими понятиями:
- Тестовый прогон — Execution (или Project)
Запуск определенного набора тестов
- Спецификация — Spec
Тестовый класс. В cucumber — это Feature
- Контейнер теста — Top Level Test
Сценарий верхнего уровня в Спецификации. В cucumber — это Scenario
- Шаг теста — Nested Test
Шаг в сценарии, который начинается с ключевого слова.
Ключевое слово обозначает этап: подготовка (Дано), воздействие (Когда), проверка ожидаемой реакции (Тогда).
В cucumber — это Step
- Вложенные Шаги — Nested Step
Это любая дополнительная информация о произведенных действиях, например аннотация @Step в Allure.
В рамках описания сценария эти шаги не несут нагрузки — они нужны для отчета, для отладки, для выяснения причин ошибки.
Kotest позволяет создавать любую вложенность, но в данном подходе ограничиваемся 4 - Шаг теста - Nested Test — дальнейшая вложенность воспринимается как шаги для отчета.
С точки зрения Форматирования теста и Review интерес представляют уровни 1 — 4.
В Gherkin есть сущность Структура Сценария (Scenario Template) — это реализация Data Driven.
В Kotest уровень 3. Контейнер теста - Top Level Test, также может являться Структурой Сценария — то есть помножиться на наборы тестовых данных.
Превращаем требования в сценарий
Допустим мы тестируем REST API сервиса и имеются требования.
Не известно, как будем отправлять запросы, как получать, десериализовать и проверять, но сейчас это не нужно.
Пишем скелет сценария:
open class KotestFirstAutomatedTesting : FreeSpec() {
private companion object {
private val log = LoggerFactory.getLogger(KotestFirstAutomatedTesting::class.java)
}
init {
"Scenario. Single case" - {
val expectedCode = 200
"Given server is up" { }
"When request prepared and sent" { }
"Then response received and has $expectedCode code" { }
}
}
}
Очень похоже на Сценарии на Gherkin
Во-первых, стоит обратить внимание, что здесь нет понятия тестовый класс, а есть спецификация (FreeSpec). И это не спроста.
Вспоминаем, что Kotlin DSL — это type-safe builder, а значит при запуске тесты сначала формируют дерево тестов / тестовых контейнеров / pre и after функций / вложенных шагов умноженных на наборы тестовых данных.
Отмечу использование интерполяции строк в имени шага "Then response received and has $expectedCode code"
Принцип работы DSL
Контейнер теста.
Используется минус после названия! Важно его не пропускать!
Тест наследуется от класса FreeSpec, в свою очередь он реализует FreeSpecRootScope:
abstract class FreeSpec(body: FreeSpec.() -> Unit = {}) : DslDrivenSpec(), FreeSpecRootScope
В FreeSpecRootScope для класса String переопределяется оператор -:
infix operator fun String.minus(test: suspend FreeScope.() -> Unit) { }
Соответсвенно запись "Scenario. Single case" - { } вызывает функцию расширения String.minus, передает внутрь функциональный тип с контекстом FreeScope и добавляет в дерево тестов контейнер.
Напомню, что в Kotlin, если лямбда-аргумент является последним в функции, то для него круглые скобки можно опускать.
Шаги теста
В том же интерфейсе FreeSpecRootScope для класса String переопределяется оператор вызова invoke
infix operator fun String.invoke(test: suspend TestContext.() -> Unit) { }
Запись "string" { } является вызовом функции расширения с аргументом функционального типа с контекстом TestContext.
Реализация теста и проверок
Вот как будет выглядеть тест с реализацией:
init {
"Scenario. Single case" - {
//region Variables
val expectedCode = 200
val testEnvironment = Server()
val tester = Client()
//endregion
"Given server is up" {
testEnvironment.start()
}
"When request prepared and sent" {
val request = Request()
tester.send(request)
}
lateinit var response: Response
"Then response received" {
response = tester.receive()
}
"And has $expectedCode code" {
response.code shouldBe expectedCode
}
}
}
Поясню некоторые момент и мотивацию
- Константы для конкретного сценария определены прямо в блоке и окружены конструкцией Idea для сворачивания
- Для обмена информацией между шагами приходится использовать переменные типа lateinit var response: Response, определенные непосредственно перед блоком, в котором они инициализируются
Kotest Assertions и Matchers
В Kotest уже есть довольно обширная библиотека Assertions and Matchers.
Зависимость testImplementation "io.kotest:kotest-assertions-core:$kotestVersion" предоставляет набор Matcher-ов, SoftAssertion и Assertion для проверки Исключений.
Есть возможность расширять ее и добавлять свои комплексные Matcher-ы, а также использовать уже готовые расширения.
Немного расширим последний шаг и добавим в него побольше проверок:
"And has $expectedCode code" {
assertSoftly {
response.asClue {
it.code shouldBe expectedCode
it.body.shouldNotBeBlank()
}
}
val assertion = assertThrows<AssertionError> {
assertSoftly {
response.asClue {
it.code shouldBe expectedCode + 10
it.body.shouldBeBlank()
}
}
}
assertion.message shouldContain "The following 2 assertions failed"
log.error("Expected assertion", assertion)
}
- assertSoftly { code }
Soft Assert из библиотеки assertions Kotest — выполнит блок кода полностью и сформирует сообщение со всеми ошибками.
- response.asClue { }
MUST HAVE для проверок в тестах. Scope функция kotlin asClue — при возникновении ошибки добавит в сообщение строковое представление всего объекта response
- Matchers
Matchers от Kotest — отличная расширяемая библиотека проверок, полностью покрывает базовые потребности.
shouldBe — infix версия проверки на равенство.
shouldBeBlank — не infix (т.к. нет аргумента) проверка на пустоту строки.
- assertThrows<AssertionError>
Статическая функция расширенной для Котлина библиотеки Junit5
inline fun <reified T : Throwable> assertThrows(noinline executable: () -> Unit) — выполняет блок, проверяет тип ожидаемого Исключения и возвращает его для дальнейших проверок
Добавляем pre / after обратные вызовы
Существует большое количество вариантов обратных вызовов на события тестов.
На текущий момент (4.3.5) все встроенные события представлены в файле io.kotest.core.spec.CallbackAliasesKt в артефакте kotest-framework-api-jvm в виде typealias:
typealias BeforeTest = suspend (TestCase) -> Unit
typealias AfterTest = suspend (Tuple2<TestCase, TestResult>) -> Unit
typealias BeforeEach = suspend (TestCase) -> Unit
typealias AfterEach = suspend (Tuple2<TestCase, TestResult>) -> Unit
typealias BeforeContainer = suspend (TestCase) -> Unit
typealias AfterContainer = suspend (Tuple2<TestCase, TestResult>) -> Unit
typealias BeforeAny = suspend (TestCase) -> Unit
typealias AfterAny = suspend (Tuple2<TestCase, TestResult>) -> Unit
typealias BeforeSpec = suspend (Spec) -> Unit
typealias AfterSpec = suspend (Spec) -> Unit
typealias AfterProject = () -> Unit
typealias PrepareSpec = suspend (KClass<out Spec>) -> Unit
typealias FinalizeSpec = suspend (Tuple2<KClass<out Spec>, Map<TestCase, TestResult>>) -> Unit
typealias TestCaseExtensionFn = suspend (Tuple2<TestCase, suspend (TestCase) -> TestResult>) -> TestResult
typealias AroundTestFn = suspend (Tuple2<TestCase, suspend (TestCase) -> TestResult>) -> TestResult
typealias AroundSpecFn = suspend (Tuple2<KClass<out Spec>, suspend () -> Unit>) -> Unit
Существует 2 вида интерфейсов, которые реализуют обратные вызовы:
- Listener
- Extension
Первый позволяет создать обратные вызов на событие теста и предоставляет immutable описание теста или результат (для after).
Второй позволяет вмешиваться в выполнение теста, что-то изменять, получать информацию о внутреннем состоянии движка, то есть небезопасен и подходит для разработки расширений, влияющих на функционал Фреймворка.
Ограничимся событиями Listener — поверьте, этого более чем достаточно.
Этот интерфейс, в свою очередь, делится еще на 2 основных:
- TestListener
- ProjectListener
Добавить свой callback можно множеством способов:
- переопределить метод в спецификации или проекте
- реализовать свой Listener и добавить его в список Слушателей явно
- реализовать свой Listener и аннотировать его @AutoScan
- вызвать метод экземпляра спецификации или проекта — наиболее удобный способ, который будем рассматривать
Обратные вызовы уровня спецификации
Самый простой способ добавить callback для одного из типов доступных событий — это вызвать одноименный метод из FreeSpec, каждый из которых принимает функциональный тип соответствующего события:
Все варианты обратных вызовов
SPL
init {
///// ALL IN INVOCATION ORDER /////
//// BEFORE ////
beforeSpec { spec ->
log.info("[BEFORE][1] beforeSpec '$spec'")
}
beforeContainer { onlyContainerTestType ->
log.info("[BEFORE][2] beforeContainer onlyContainerTestType '$onlyContainerTestType'")
}
beforeEach { onlyTestCaseType ->
log.info("[BEFORE][3] beforeEach onlyTestCaseType '$onlyTestCaseType'")
}
beforeAny { containerOrTestCaseType ->
log.info("[BEFORE][4] beforeAny containerOrTestCaseType '$containerOrTestCaseType'")
}
beforeTest { anyTestCaseType ->
log.info("[BEFORE][5] beforeTest anyTestCaseType '$anyTestCaseType'")
}
//// AFTER ////
afterTest { anyTestCaseTypeWithResult ->
log.info("[AFTER][1] afterTest anyTestCaseTypeWithResult '$anyTestCaseTypeWithResult'")
}
afterAny { containerOrTestCaseTypeAndResult ->
log.info("[AFTER][2] afterAny containerOrTestCaseTypeAndResult '$containerOrTestCaseTypeAndResult'")
}
afterEach { onlyTestCaseTypeAndResult ->
log.info("[AFTER][3] afterEach onlyTestCaseTypeAndResult '$onlyTestCaseTypeAndResult'")
}
afterContainer { onlyContainerTestTypeAndResult ->
log.info("[AFTER][4] afterContainer onlyContainerTestTypeAndResult '$onlyContainerTestTypeAndResult'")
}
afterSpec { specWithoutResult ->
log.info("[AFTER][5] afterSpec specWithoutResult '$specWithoutResult'")
}
//// AT THE END ////
finalizeSpec {specWithAllResults ->
log.info("[FINALIZE][LAST] finalizeSpec specWithAllResults '$specWithAllResults'")
}
"Scenario" - { }
}
В коде выше все обратные вызовы определены в порядке выполнения до и после теста.
before
- beforeSpec
Выполняется сразу после создания экземпляра класс FreeSpec и перед выполнением первого теста, имеет один аргумент — Spec описание спецификации
- beforeContainer
Выполняется только перед контейнером теста TestType.Container, имеет один аргумент — TestCase описание контейнера теста
- beforeEach
Выполняется только перед шагами (тестами) в контейнере теста TestType.Test, имеет один аргумент — TestCase описание шага теста (вложенный сценарий)
- beforeAny
Выполняется перед контейнером теста TestType.Container и перед шагами TestType.Test, имеет один аргумент — TestCase описание шага или контейнера
- beforeTest
Выполняется перед любой сущностью TestCase будь то контейнер или шаг, или новый TestType которого пока не существует.
Фактически сейчас это beforeAny. Нужен для сохранения совместимости с прошлыми версиями (когда не было TestType) и с будущими (когда будут новые TestType)
after
- afterTest
Аналогично beforeTest только после.
Имеет аргумент пару — TestCase + TestResult
- afterAny
Аналогично beforeAny только после.
Имеет аргумент пару — TestCase + TestResult
- afterEach
Аналогично beforeEach только после.
Имеет аргумент пару — TestCase + TestResult
- afterContainer
Аналогично beforeContainer только после.
Имеет аргумент пару — TestCase + TestResult
- afterSpec
Аналогично beforeSpec только после.
Имеет аргумент пару — Spec
finalizeSpec
Выполняется сразу после окончания работы всех тестов в Спецификации.
Имеет аргумент пару — класс спецификации KClass<out Spec> + отображение всех тестов и результатов Map<TestCase, TestResult>
Обратные вызовы уровня проекта
Kotest предоставляет возможность определить callback на события всего запуска тестов.
Их два:
- beforeAll
Выполняется перед первым тестом в запуске
- afterAll
Выполняется после окончания всех тестов
Определить можно с помощью реализации ProjectListener и добавления в список слушателей проекта в конфигурации проекта, в AbstractProjectConfig либо одиночке Project.
Также можно переопределить одноименные методы в AbstractProjectConfig — в большинстве случаев предпочтительный способ:
object ProjectConfig : AbstractProjectConfig() {
private val log = LoggerFactory.getLogger(ProjectConfig::class.java)
override fun beforeAll() {
log.info("[BEFORE PROJECT] beforeAll")
}
override fun afterAll() {
log.info("[AFTER PROJECT] afterAll")
}
}
Делаем Data Driven Test
В пакете io.kotest.data предоставлен набор классов и функций для организации Data Driven Testing
Создадим простейший тест c Data Provider-ом:
init {
"Scenario. Single case" - {
val testEnvironment = Server()
val tester = Client()
"Given server is up. Will execute only one time" {
testEnvironment.start()
}
forAll(
row(1, UUID.randomUUID().toString()),
row(2, UUID.randomUUID().toString())
) { index, uuid ->
"When request prepared and sent [$index]" {
tester.send(Request(uuid))
}
"Then response received [$index]" {
tester.receive().code shouldBe 200
}
}
}
}
Выглядит довольно просто, а главное понятно (наверное)
- Начало, как в обычном тесте — определяем контейнер для тестов.
- Следующий шаг Given server is up выполнится также, как в обычном тесте — единожды.
- Далее следует функция forAll. Она принимает наборы Row и функциональный блок, в котором продолжаем декларировать шаги теста.
- Функция row определяет один набор тестовых данных для одной итерации.
В пакете файле io.kotest.data.rows.kt определено 22 функции для разного кол-ва данных в одном наборе.
Если этого не хватает, то есть возможность реализовать свою последовательность в подходе Property Based Testing (это выходит за рамки этой статьи)
- В итоге имеем:
forAll(
row(1, UUID.randomUUID().toString()),
row(2, UUID.randomUUID().toString())
) { index, uuid -> block }
2 итерации со своим набором тестовых данных.
В каждом наборе 2 значения. Функциональный блок, который выполнится 2 раза.
Существует важное ограничение на имена в рамках контейнера — все имена шагов должны быть уникальными.
Поэтому в шагах добавлены уникальные индексы [$index].
Можно обойтись без индекса и печатать uuid в каждом шаге — индекс используется только для упорядоченности в отчете.
Выводы
Во-первых, привожу ссылку на все примеры qa-kotest-articles/kotest-first.
По итогу имеем полноценный фреймворк для запуска тестов.
Широкий контроль жизненного цикла всего запуска, спецификации, каждого сценария и его шагов.
Результаты запуска трансформируются в стандартные junit отчеты, все события публикуются в слушатели junit для корректного отображения в консоли Idea.
Также имеется Idea плагин.
Data Driven без всяких аннотаций или дополнительных классов.
Все это дело похоже на Groovy Spoke, но только для Kotlin.
Отмечу удобную документацию в виде сайта kotest.io — еще в версии 4.2.0 документация была в множестве readme.md проекта на github.
Планы
В планах написание следующих частей, которые покроют тему 'Kotlin. Автоматизация тестирования':
- Kotest. Расширения, конфигурирование проекта, конфигурирование спецификации и конфигурирование тестов, тэги и фабрики тестов, Property Based Testing
- Spring Test. Интеграция с Kotest. Конфигурирование тестового контекста и контроль жизненного цикла бинов.
- Ожидания Awaitility. Retrofit для тестирования API. Работа c БД через Spring Data Jpa.
- Gradle. Масштабируемая и распределенная структура множества проектов авто-тестов.
- Управление окружением. TestContainers, gradle compose plugin, kubernetes java api + helm
===========
Источник:
habr.com
===========
Похожие новости:
- [Тестирование IT-систем, Виртуализация, Разработка под Linux] Автоматизация системных тестов на базе QEMU (Часть 1/2)
- [Gradle, Kotlin, Разработка мобильных приложений, Разработка под Android] Знакомство с App Gallery. Создаем аккаунт разработчика
- [Виртуализация, Amazon Web Services, Тестирование веб-сервисов, Облачные сервисы] Работа в Amazon WorkSpaces: опыт развертывания и настройки
- [Информационная безопасность, Тестирование IT-систем] Как плохо настроенная БД позволила захватить целое облако с 25 тысячами хостов
- [Программирование, Scala] Scala мертва?
- [Разработка веб-сайтов, Тестирование веб-сервисов] 5 самых неприятных фич на сайтах для слепого человека (перевод)
- [Конференции, Тестирование IT-систем, Управление разработкой] 30 сентября приглашаем на круглый стол QA&SDET онлайн
- [Информационная безопасность, Совершенный код, Управление продуктом, Софт] Строим безопасную разработку в ритейлере. Опыт одного большого проекта
- [Python, Тестирование веб-сервисов] Как читать файлы конфигурации в тестах с Selenium на Python (перевод)
- [Удалённая работа, Разработка под e-commerce, Микросервисы, Тестирование веб-сервисов] Что помогло нам быстро перестроиться на онлайн-торговлю в новых условиях
Теги для поиска: #_testirovanie_itsistem (Тестирование IT-систем), #_testirovanie_vebservisov (Тестирование веб-сервисов), #_kotlin, #_kotlin, #_kotest, #_qa_automation, #_assertion, #_matcher, #_data_driven_testing, #_behavior_driven_development, #_bdd, #_functional_testing, #_kotlin_test, #_testirovanie_itsistem (
Тестирование IT-систем
), #_testirovanie_vebservisov (
Тестирование веб-сервисов
), #_kotlin
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:50
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Хочу поделиться опытом создания системы автоматизации функционального тестирования на языке на Kotlin. Основой для создания / конфигурирования / запуска / контроля выполнения тестов — будет набирающий популярность молодой фреймворк Kotest (в прошлом Kotlin Test). Проанализировав все популярные варианты для Kotlin — выяснилось, что есть всего два "нативных": Либо бесконечное кол-во из Java мира: Junit4/5, TestNG, Cucumber JVM или другие BDD фреймворки. Выбора пал на Kotest с бОльшим кол-вом "лайков" на GitHub, чем у Spek. Руководств по автоматизации тестирования на Kotlin, особенно в сочетании с Kotest — немного (пока). Думаю, что написать цикл статей про Kotest, а также про организацию проекта авто-тестов, сборку, запуск и сопутствующие технологии — хорошая идея. По итогу должно получиться емкое руководство — как создать систему, или даже эко-систему, автоматизации функционального тестирования на языке Kotlin и платформе Kotest. Минусы Сходу определимся по глобальным минусам и учтем, что проект быстро развивается. Те проблемы, что есть в актуальной на время написания статьи версии 4.2.5, уже могут быть исправлены в свежих релизах. Первым, очевидным, минусом является очень быстрое изменение минорных и мажорных версий. Еще в марте 2020 фреймворк назывался KotlinTest, с версии 4.0.0 поменял название всех пакетов, потерял совместимость с плагином Idea и был переименован в Kotest, а также стал мульти-платформенным. После обновления с версии 4.0.7 до 4.1 перестали работать расширения, написанные ранее, также были изменены названия базовых модулей и много чего еще, то есть принцип семантического версионирования нарушился между минорными версиями 4.0 и 4.1. Это немыслимо для Java мира — это что-то из JS. У инструмента пока небольшое комьюнити. В пользу широкого функционала иногда приносится в жертву продуманный дизайн. Не все предоставляемые стили написания тестов адекватно транслируются в отчет. На текущий момент не корректно отображаются репорты для data-driven и property-based тестов. Шаблоны и фабрики тестов работают не как ожидается. Встроенное расширение allure совсем никуда не годится (к примеру, оно пытается обрабатывать аннотации, которыми в принципе невозможно аннотировать тестовые DSL методы). Однако ни одного критического или блокирующего дефекта я не встретил. Почему Kotest? Многие разработчики на Kotlin не заморачиваются с выбором тестового фреймворка и продолжают использовать Junit4 или Junit5. Для них тесты — это, как правило, класс, помеченный аннотацией @SpringBootTest, набор методов с аннотацией @Test, возможно методы before и beforeClass с соответствующими аннотациями. Для полноценных функциональных e2e тестов этого недостаточно. Нужен инструмент предоставляющий возможность создавать понятные тесты на основе требований, удобную организацию проверок, тестовых данных и отчетности. Так вот Kotest позволяет:
Начинаем создавать тест Какой стиль выбрать? Kotest дает возможность выбора между несколькими вариантами DSL для формирования структуры тестов. Самый базовый и простой String Spec — идеально подойдет для написания unit-тестов с одним уровнем вложенности. Для полноценных функциональных авто-тестов нужно что-то посложнее: по структуре схожее с Gherkin, но менее формализованное, особенно по ключевым словам. После долгих экспериментов я остановился на стиле FreeSpec. Используя Kotest я рекомендую продолжать писать тесты в BDD стиле, как в языке Gherkin (Cucumber). FreeStyle не накладывает ограничений на именование тестов, ключевые слова и вложенность, поэтому эти вещи нужно контролировать на уровне code-style, best practice, обучения и Merge-Request`ов. Иерархия тестовых сущностей В нашем подходе будет 5 базовых тестовых сущностей (или уровней) в рамках Kotest. Важно определить это сейчас, потому что в дальнейшем оперировать я буду этими понятиями:
Ключевое слово обозначает этап: подготовка (Дано), воздействие (Когда), проверка ожидаемой реакции (Тогда). В cucumber — это Step
В рамках описания сценария эти шаги не несут нагрузки — они нужны для отчета, для отладки, для выяснения причин ошибки. Kotest позволяет создавать любую вложенность, но в данном подходе ограничиваемся 4 - Шаг теста - Nested Test — дальнейшая вложенность воспринимается как шаги для отчета. С точки зрения Форматирования теста и Review интерес представляют уровни 1 — 4. В Gherkin есть сущность Структура Сценария (Scenario Template) — это реализация Data Driven. В Kotest уровень 3. Контейнер теста - Top Level Test, также может являться Структурой Сценария — то есть помножиться на наборы тестовых данных. Превращаем требования в сценарий Допустим мы тестируем REST API сервиса и имеются требования. Не известно, как будем отправлять запросы, как получать, десериализовать и проверять, но сейчас это не нужно. Пишем скелет сценария: open class KotestFirstAutomatedTesting : FreeSpec() {
private companion object { private val log = LoggerFactory.getLogger(KotestFirstAutomatedTesting::class.java) } init { "Scenario. Single case" - { val expectedCode = 200 "Given server is up" { } "When request prepared and sent" { } "Then response received and has $expectedCode code" { } } } } Очень похоже на Сценарии на Gherkin Во-первых, стоит обратить внимание, что здесь нет понятия тестовый класс, а есть спецификация (FreeSpec). И это не спроста. Вспоминаем, что Kotlin DSL — это type-safe builder, а значит при запуске тесты сначала формируют дерево тестов / тестовых контейнеров / pre и after функций / вложенных шагов умноженных на наборы тестовых данных. Отмечу использование интерполяции строк в имени шага "Then response received and has $expectedCode code" Принцип работы DSL Контейнер теста. Используется минус после названия! Важно его не пропускать!
abstract class FreeSpec(body: FreeSpec.() -> Unit = {}) : DslDrivenSpec(), FreeSpecRootScope
В FreeSpecRootScope для класса String переопределяется оператор -: infix operator fun String.minus(test: suspend FreeScope.() -> Unit) { }
Соответсвенно запись "Scenario. Single case" - { } вызывает функцию расширения String.minus, передает внутрь функциональный тип с контекстом FreeScope и добавляет в дерево тестов контейнер. Напомню, что в Kotlin, если лямбда-аргумент является последним в функции, то для него круглые скобки можно опускать. Шаги теста В том же интерфейсе FreeSpecRootScope для класса String переопределяется оператор вызова invoke infix operator fun String.invoke(test: suspend TestContext.() -> Unit) { }
Запись "string" { } является вызовом функции расширения с аргументом функционального типа с контекстом TestContext. Реализация теста и проверок Вот как будет выглядеть тест с реализацией: init {
"Scenario. Single case" - { //region Variables val expectedCode = 200 val testEnvironment = Server() val tester = Client() //endregion "Given server is up" { testEnvironment.start() } "When request prepared and sent" { val request = Request() tester.send(request) } lateinit var response: Response "Then response received" { response = tester.receive() } "And has $expectedCode code" { response.code shouldBe expectedCode } } } Поясню некоторые момент и мотивацию
Kotest Assertions и Matchers В Kotest уже есть довольно обширная библиотека Assertions and Matchers. Зависимость testImplementation "io.kotest:kotest-assertions-core:$kotestVersion" предоставляет набор Matcher-ов, SoftAssertion и Assertion для проверки Исключений. Есть возможность расширять ее и добавлять свои комплексные Matcher-ы, а также использовать уже готовые расширения. Немного расширим последний шаг и добавим в него побольше проверок: "And has $expectedCode code" {
assertSoftly { response.asClue { it.code shouldBe expectedCode it.body.shouldNotBeBlank() } } val assertion = assertThrows<AssertionError> { assertSoftly { response.asClue { it.code shouldBe expectedCode + 10 it.body.shouldBeBlank() } } } assertion.message shouldContain "The following 2 assertions failed" log.error("Expected assertion", assertion) }
Добавляем pre / after обратные вызовы Существует большое количество вариантов обратных вызовов на события тестов. На текущий момент (4.3.5) все встроенные события представлены в файле io.kotest.core.spec.CallbackAliasesKt в артефакте kotest-framework-api-jvm в виде typealias: typealias BeforeTest = suspend (TestCase) -> Unit
typealias AfterTest = suspend (Tuple2<TestCase, TestResult>) -> Unit typealias BeforeEach = suspend (TestCase) -> Unit typealias AfterEach = suspend (Tuple2<TestCase, TestResult>) -> Unit typealias BeforeContainer = suspend (TestCase) -> Unit typealias AfterContainer = suspend (Tuple2<TestCase, TestResult>) -> Unit typealias BeforeAny = suspend (TestCase) -> Unit typealias AfterAny = suspend (Tuple2<TestCase, TestResult>) -> Unit typealias BeforeSpec = suspend (Spec) -> Unit typealias AfterSpec = suspend (Spec) -> Unit typealias AfterProject = () -> Unit typealias PrepareSpec = suspend (KClass<out Spec>) -> Unit typealias FinalizeSpec = suspend (Tuple2<KClass<out Spec>, Map<TestCase, TestResult>>) -> Unit typealias TestCaseExtensionFn = suspend (Tuple2<TestCase, suspend (TestCase) -> TestResult>) -> TestResult typealias AroundTestFn = suspend (Tuple2<TestCase, suspend (TestCase) -> TestResult>) -> TestResult typealias AroundSpecFn = suspend (Tuple2<KClass<out Spec>, suspend () -> Unit>) -> Unit Существует 2 вида интерфейсов, которые реализуют обратные вызовы:
Первый позволяет создать обратные вызов на событие теста и предоставляет immutable описание теста или результат (для after). Второй позволяет вмешиваться в выполнение теста, что-то изменять, получать информацию о внутреннем состоянии движка, то есть небезопасен и подходит для разработки расширений, влияющих на функционал Фреймворка. Ограничимся событиями Listener — поверьте, этого более чем достаточно. Этот интерфейс, в свою очередь, делится еще на 2 основных:
Добавить свой callback можно множеством способов:
Обратные вызовы уровня спецификации Самый простой способ добавить callback для одного из типов доступных событий — это вызвать одноименный метод из FreeSpec, каждый из которых принимает функциональный тип соответствующего события: Все варианты обратных вызововSPLinit {
///// ALL IN INVOCATION ORDER ///// //// BEFORE //// beforeSpec { spec -> log.info("[BEFORE][1] beforeSpec '$spec'") } beforeContainer { onlyContainerTestType -> log.info("[BEFORE][2] beforeContainer onlyContainerTestType '$onlyContainerTestType'") } beforeEach { onlyTestCaseType -> log.info("[BEFORE][3] beforeEach onlyTestCaseType '$onlyTestCaseType'") } beforeAny { containerOrTestCaseType -> log.info("[BEFORE][4] beforeAny containerOrTestCaseType '$containerOrTestCaseType'") } beforeTest { anyTestCaseType -> log.info("[BEFORE][5] beforeTest anyTestCaseType '$anyTestCaseType'") } //// AFTER //// afterTest { anyTestCaseTypeWithResult -> log.info("[AFTER][1] afterTest anyTestCaseTypeWithResult '$anyTestCaseTypeWithResult'") } afterAny { containerOrTestCaseTypeAndResult -> log.info("[AFTER][2] afterAny containerOrTestCaseTypeAndResult '$containerOrTestCaseTypeAndResult'") } afterEach { onlyTestCaseTypeAndResult -> log.info("[AFTER][3] afterEach onlyTestCaseTypeAndResult '$onlyTestCaseTypeAndResult'") } afterContainer { onlyContainerTestTypeAndResult -> log.info("[AFTER][4] afterContainer onlyContainerTestTypeAndResult '$onlyContainerTestTypeAndResult'") } afterSpec { specWithoutResult -> log.info("[AFTER][5] afterSpec specWithoutResult '$specWithoutResult'") } //// AT THE END //// finalizeSpec {specWithAllResults -> log.info("[FINALIZE][LAST] finalizeSpec specWithAllResults '$specWithAllResults'") } "Scenario" - { } } В коде выше все обратные вызовы определены в порядке выполнения до и после теста. before
after
finalizeSpec Выполняется сразу после окончания работы всех тестов в Спецификации. Имеет аргумент пару — класс спецификации KClass<out Spec> + отображение всех тестов и результатов Map<TestCase, TestResult> Обратные вызовы уровня проекта Kotest предоставляет возможность определить callback на события всего запуска тестов. Их два:
Определить можно с помощью реализации ProjectListener и добавления в список слушателей проекта в конфигурации проекта, в AbstractProjectConfig либо одиночке Project. Также можно переопределить одноименные методы в AbstractProjectConfig — в большинстве случаев предпочтительный способ: object ProjectConfig : AbstractProjectConfig() {
private val log = LoggerFactory.getLogger(ProjectConfig::class.java) override fun beforeAll() { log.info("[BEFORE PROJECT] beforeAll") } override fun afterAll() { log.info("[AFTER PROJECT] afterAll") } } Делаем Data Driven Test В пакете io.kotest.data предоставлен набор классов и функций для организации Data Driven Testing Создадим простейший тест c Data Provider-ом: init {
"Scenario. Single case" - { val testEnvironment = Server() val tester = Client() "Given server is up. Will execute only one time" { testEnvironment.start() } forAll( row(1, UUID.randomUUID().toString()), row(2, UUID.randomUUID().toString()) ) { index, uuid -> "When request prepared and sent [$index]" { tester.send(Request(uuid)) } "Then response received [$index]" { tester.receive().code shouldBe 200 } } } } Выглядит довольно просто, а главное понятно (наверное)
2 итерации со своим набором тестовых данных. В каждом наборе 2 значения. Функциональный блок, который выполнится 2 раза. Существует важное ограничение на имена в рамках контейнера — все имена шагов должны быть уникальными. Поэтому в шагах добавлены уникальные индексы [$index]. Можно обойтись без индекса и печатать uuid в каждом шаге — индекс используется только для упорядоченности в отчете. Выводы Во-первых, привожу ссылку на все примеры qa-kotest-articles/kotest-first. По итогу имеем полноценный фреймворк для запуска тестов. Широкий контроль жизненного цикла всего запуска, спецификации, каждого сценария и его шагов. Результаты запуска трансформируются в стандартные junit отчеты, все события публикуются в слушатели junit для корректного отображения в консоли Idea. Также имеется Idea плагин. Data Driven без всяких аннотаций или дополнительных классов. Все это дело похоже на Groovy Spoke, но только для Kotlin. Отмечу удобную документацию в виде сайта kotest.io — еще в версии 4.2.0 документация была в множестве readme.md проекта на github. Планы В планах написание следующих частей, которые покроют тему 'Kotlin. Автоматизация тестирования':
=========== Источник: habr.com =========== Похожие новости:
Тестирование IT-систем ), #_testirovanie_vebservisov ( Тестирование веб-сервисов ), #_kotlin |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 17:50
Часовой пояс: UTC + 5