[Тестирование IT-систем, Тестирование веб-сервисов, Kotlin] Kotlin. Автоматизация тестирования (часть 1). Kotest: Начало

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
23-Сен-2020 20:35


Хочу поделиться опытом создания системы автоматизации функционального тестирования на языке на 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 интерес представляют уровни 14.
В 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
===========

Похожие новости: Теги для поиска: #_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