Русский
Русский
English
Статистика
Реклама

Qa automation

Kotlin. Автоматизация тестирования (Часть 2). Kotest. Deep Diving

20.02.2021 10:19:11 | Автор: admin

Kotest


Продолжение цикла публикаций про автоматизацию функционального тестирования на Kotlin
с использованием фреймворка Kotest совместно с наиболее полезными дополнительными библиотеками, существенно облегчающими и ускоряющими создание тестов.


В этой части мы углубимся в возможности Kotest:


  • покажу все варианты группировки тесты
  • расскажу про последовательность выполнения тестов и спецификаций
  • изучим возможности параллельного запуска
  • настроим таймауты на выполнение тестов
  • проговорим про ожидания и Flaky-тесты
  • рассмотрим использование Фабрик тестов
  • и напоследок исследуем тему Property Testing

Все части руководства:



О себе


Я являюсь QA Лидом на одном из проектов Мир Plat.Form (НСПК). Проект зародился около года назад и уже вырос до четырех команд, где трудится в общей сложности около 10 разработчиков в тестировании (SDET), без учета остальных участников в лице аналитиков, разработчиков и технологов.
Наша задача автоматизировать функциональные тесты на уровне отдельных сервисов, интеграций между ними и E2E до попадания функционала в master всего порядка 30 микро-сервисов. Взаимодействие между сервисами Kafka, внешний API REST, а также 2 фронтовых Web приложения.
Разработка самой системы и тестов ведется на языке Kotlin, а движок для тестов был выбран Kotest.


В данной статье и в остальных публикациях серии я максимально подробно рассказываю о тестовом Движке и вспомогательных технологиях в формате Руководства/Tutorial.


Мотивация и цели


На основе первой части руководства можно вполне успешно создавать сценарии, расширять покрытие. Но с увеличением кол-ва тестов и функциональности целевого приложения неизбежно растет и сложность тестов.
Чтобы не изобретать "велосипеды" и не добавлять в проект тестов лишние зависимости хороший QA обязан детально знать функционал тестового движка и всех сторонних библиотек.
Это позволит в дальнейшем проектировать простую, масштабируемую и расширяемую архитектуру каркаса и тестов. А также успешно интегрировать популярные библиотеки.


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


  • Spring Core/Test. Dependencies Injection и конфигурирование профилей для тестов
  • Spring Data JPA. Работа с БД
  • TestContainers. Управление Docker контейнерами
  • Allure. Отчетность
  • Awaitility. Ожидания

Группировка тестов


В публикации много примеров с пояснениями двух видов: комментарии в коде либо объяснения вне блока кода со ссылками на конкретные места примера.
Куски кода больше половины стандартного экрана скрыты спойлером.
Рассматриваемая версия Kotest 4.3.2

Рекомендую на самых ранних стадиях развития проекта с тестами продумать группировку тестов. Самые базовые критерии группировки, которые первыми приходят на ум:


  1. По уровню. Модульные -> На один сервис (в контексте микро-сервисов) -> Интеграционные -> E2E


  2. По платформе. Windows\Linux\MacOS | Desktop\Web\IOS\Android


  3. По функциональности. Frontend\Backend В контексте целевого приложения: Авторизация\Администрирование\Отчетность...


  4. По релизам. Sprint-1\Sprint-2 1.0.0\2.0.0



Далее описание реализации группировки в Kotest


Теги


Для группировки тестов используются теги объекты класса abstract class io.kotest.core.Tag. Есть несколько вариантов декларирования меток:


  • расширить класс Tag, тогда в качестве имени метки будет использовано simpleName расширяющего класса, либо переопределенное свойство name.
  • использовать объект класса io.kotest.core.NamedTag, где имя метки передается в конструкторе.
  • создать String константу и использовать в аннотации @io.kotest.core.annotation.Tags, однако в прошлых версиях Kotest эта аннотация принимала тип KClass<T> класса тегов, а сейчас String имя тега.

Ниже представлены все варианты декларирования тегов:


/** * TAG for annotation @Tag only. */const val LINUX_TAG = "Linux"/** * Name will be class simple name=Windows */object Windows : Tag()/** * Override name to Linux. */object LinuxTag : Tag() {    override val name: String = LINUX_TAG}/** * Create [NamedTag] object with name by constructor. * Substitute deprecated [io.kotest.core.StringTag] */val regressTag = NamedTag("regress")

Применить тег к тесту можно несколькими путями:


  • через аннотацию @Tags на классе спецификации
  • extension функцию String.config в тесте или контейнере теста
  • переопределив метод tags(): Set<Tag> у спецификации

Однако, последний вариант я не рекомендую, т.к. тестовому движку для получения тега и проверки придется создать экземпляр класса Spec и выполнить init блок, а еще выполнить все инъекции зависимостей, если используется Dependency injection в конструкторе.

Пример с 2-мя классами спецификаций:


@Tags(LINUX_TAG) // Аннотацияclass LinuxSpec : FreeSpec() {    init {        "1-Test for Linux" { }                                        /* конфигурация теста String.config */        "2-Test for Linux and Regress only".config(tags = setOf(regressTag)) { }    }}class WindowsSpec : FreeSpec() {    /** Override tags method */    override fun tags(): Set<Tag> = setOf(Windows) // переопределение метода    init {        "Test for Windows" { }    }}

Остается задать выражение тегов для запуска ожидаемого набора тестов через системную переменную -Dkotest.tags=<выражение для выборки тестов по тегам>.
В выражении можно использовать ограниченный набор операторов: (, ), |, &


Теги чувствительны к регистру

Привожу несколько вариантов запуска в виде Gradle task


Набор Gradle Tasks для запуска групп тестов
// Используется встроенный в Gradle фильтр тестов по пакетам без фильтрации теговtask gradleBuildInFilterTest(type: Test) {    group "test"    useJUnitPlatform()    systemProperties = System.properties    filter { includeTestsMatching("ru.iopump.qa.sample.tag.*") }}// Запустить только тест с тегами regress (в тесте) и Linux (в аннотации спецификации) в LinuxSpectask linuxWithRegressOnlyTest(type: Test) {    group "test"    useJUnitPlatform()    systemProperties = System.properties + ["kotest.tags": "Linux & regress"]}// Запустить 2 теста с тегом Linux (в аннотации спецификации) в LinuxSpectask linuxAllTest(type: Test) {    group "test"    useJUnitPlatform()    systemProperties = System.properties + ["kotest.tags": "Linux"]}// Исключить из запуска тесты с тегом Linux, а также Windows. То есть запуститься 0 тестов task noTest(type: Test) {    group "test"    useJUnitPlatform()    systemProperties = System.properties + ["kotest.tags": "!Linux & !Windows"]}// Запустить тесты, у которых имеется тег Linux либо Windows. То есть все тестыtask linuxAndWindowsTest(type: Test) {    group "test"    useJUnitPlatform()    systemProperties = System.properties + ["kotest.tags": "Linux | Windows"]}

Обращаю внимание на 2 момента:


  • Kotest запускается через Junit5 Runner, поэтому декларирован useJUnitPlatform()
  • Gradle не копирует системные переменные в тест, поэтому необходимо явно указать systemProperties = System.properties

Условный запуск


В коде спецификации можно задать динамические правила включения/выключения тестов двумя путями:


  • на уровне спецификации через аннотацию @io.kotest.core.annotation.EnabledIf и реализацию интерфейса EnabledCondition
  • на уровне теста через расширение String.config(enabledIf = (TestCase) -> Boolean)
    Вот пример:

/** [io.kotest.core.annotation.EnabledIf] annotation with [io.kotest.core.annotation.EnabledCondition] */@EnabledIf(OnCICondition::class) // Аннотация принимает класс с логикой включениеclass CIOnlySpec : FreeSpec() {    init {                            /* Логика включения передается в конфигурацию теста */        "Test for Jenkins".config(enabledIf = jenkinsTestCase) { }    }}/** typealias EnabledIf = (TestCase) -> Boolean */val jenkinsTestCase: io.kotest.core.test.EnabledIf = { testCase: TestCase -> testCase.displayName.contains("Jenkins") }/** Separate class implementation [io.kotest.core.annotation.EnabledCondition] */class OnCICondition : EnabledCondition {    override fun enabled(specKlass: KClass<out Spec>) = System.getProperty("CI") == "true"}

Тест запустится, если:


  1. Среди системных переменных будет переменная CI=true
  2. В отображаемом имени TestCase встретится строка Jenkins

Функционал не самый используемый.
Есть более простой, но менее гибкий вариант через параметр enabled.
Например: "My test".config(enabled = System.getProperty("CI") == "true") { }

Последовательность выполнения


Уровень спецификации


По-умолчанию спецификации выполняются в порядке загрузки классов JVM. Это зависит от платформы, но скорее всего порядок будет алфавитный. Есть возможность задать порядок через конфигурацию уровня проекта используя enum io.kotest.core.spec.SpecExecutionOrder


object ProjectConfig : AbstractProjectConfig() {    override val specExecutionOrder = SpecExecutionOrder.Annotated}

  • SpecExecutionOrder.Undefined. Используется по-умолчанию и зависит от загрузки классов на платформе.
  • SpecExecutionOrder.Lexicographic. В алфавитном порядке имен классов спецификаций
  • SpecExecutionOrder.Random. В случайном порядке.
  • SpecExecutionOrder.Annotated. На основе аннотаций @Order над классами спецификаций с номерным аргументом. Меньше номер раньше выполнение. Не помеченные выполняются в конце по стратегии Undefined
  • SpecExecutionOrder.FailureFirst. Новая стратегия. Сначала выполняет упавшие в предыдущем прогоне тесты, а остальные спецификации по стратегии Lexicographic

Для использования FailureFirst необходимо включить сохранение результатов прогона в конфигурации проекта. По-умолчанию результаты сохраняются по пути на файловой системе ./.kotest/spec_failures в директории проекта.


object ProjectConfig : AbstractProjectConfig() {    override val specExecutionOrder = SpecExecutionOrder.FailureFirst    /**     * Save execution results to file for [SpecExecutionOrder.FailureFirst] strategy.     * File location: [io.kotest.core.config.Configuration.specFailureFilePath] = "./.kotest/spec_failures"     */    override val writeSpecFailureFile = true}

Привожу пример использования стратегии Annotated:


object ProjectConfig : AbstractProjectConfig() {    override val specExecutionOrder = SpecExecutionOrder.Annotated}@Order(Int.MIN_VALUE) // Аннотация на классеclass FirstSpec : FreeSpec() {    init {        "FirstSpec-Test" { }    }}@Order(Int.MIN_VALUE + 1) // Аннотация на классеclass SecondSpec : FreeSpec() {    init {        "SecondSpec-Test" { }    }}@Order(Int.MAX_VALUE) // Аннотация на классеclass LastSpec : FreeSpec() {    init {        "LastSpec-Test" { }    }}

Порядок выполнения будет такой: FirstSpec -> SecondSpec -> LastSpec. Чем больше число в @Order тем позже выполнение.


Уровень тестов


Выше была рассмотрена последовательность запуска классов спецификаций (тестовых классов). Но также доступна настройка последовательности запуска тест-кейсов в рамках одного класса.
По-умолчанию внутри класса спецификации тесты запускаются в порядке следования в коде TestCaseOrder.Sequential и это наиболее удачный вариант. Однако и этот порядок можно переопределить, если есть уверенность, что каждый тест абсолютно независим.


Для всего проекта можно настроить последовательность в конфигурации проекта:


object ProjectConfig : AbstractProjectConfig() {    override val testCaseOrder: TestCaseOrder = TestCaseOrder.Random}

В рамках одного класса через переопределение метода testCaseOrder:


class TestOrderingSpec : FreeSpec() {    override fun testCaseOrder(): TestCaseOrder = TestCaseOrder.Lexicographic}

Параллельность


Очень интересная и сложная тема, как в контексте разработки, так и в контексте тестирования.
Рекомендую ознакомиться с книгой Java Concurrency in Practice она как раз недавно появилась на русском языке.
Основой для принятия решения запуска тестов параллельно служит свойство теста быть независимым в контексте Фреймворка тестов и тестируемой системы.
Недостаточно обеспечить очистку состояния тестируемой системы, в которое она перешла после выполнения теста,- необходимо обеспечить неизменное состояние во время выполнения теста, либо гарантию, что изменение состояния не имеет инвариантов с другими воздействиями на систему в других тестах.


Для гарантии изолированности необходимо:


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

Есть системы, которые потенциально независимо обрабатывают запросы или сессии, можно положиться на это, однако 100% гарантии никто не даст.
Тест может воздействовать на систему в обход публичного API (например что-то записать в БД напрямую), что также может нарушить изоляцию.


Kotest не решает проблемы обеспечения условий для параллельного запуска, но предоставляет инструменты для организации параллельного выполнения на уровне тестов и спецификаций


Уровень тестов


Можно задать кол-во потоков для тестов в рамках одного класса спецификации переопределив метод fun threads()


Пример спецификации с 2-мя тестами и 2-мя потоками:


class OneParallelOnTestLevelSpec : FreeSpec() {    private companion object {        private val log = LoggerFactory.getLogger(OneParallelOnTestLevelSpec::class.java)    }    override fun threads(): Int = 2    init {        "parallel on test level 1" {            log.info("test 1 started")            delay(500)            log.info("test 1 finished")        }        "parallel on test level 2" {            log.info("test 2 started")            delay(1000)            log.info("test 2 finished")        }    }}

Запустим с помощью gradle задачи:


task parallelismTest(type: Test) {    group "test"    useJUnitPlatform()    filter { includeTestsMatching("ru.iopump.qa.sample.parallelism.OneParallelOnTestLevelSpec") }}

Имеем вывод:


21:15:44:979 OneParallelOnTestLevelSpec - test 2 started21:15:44:979 OneParallelOnTestLevelSpec - test 1 started21:15:45:490 OneParallelOnTestLevelSpec - test 1 finished21:15:45:990 OneParallelOnTestLevelSpec - test 2 finished

По логу видно, что test 1 и test 2 запустились одновременно в 21:15:44:979 и завершились:
первый в 21:15:45:490, второй через 500мс, все по-плану.


Уровень спецификации


Чтобы запустить классы спецификаций в несколько потоков нужно задать настройки параллельности до их запуска, то есть на уровне конфигурации проекта:


object ProjectConfig : AbstractProjectConfig() {    override val parallelism: Int = 3}

Либо через системную переменную -Dkotest.framework.parallelism=3 или прямо в задаче gradle:


task parallelismTest(type: Test) {    group "test"    useJUnitPlatform()    doFirst { systemProperties = System.properties + ["kotest.framework.parallelism": 3] }    filter { includeTestsMatching("ru.iopump.qa.sample.parallelism.*") }}

Используем одну спецификацию с прошлого раздела OneParallelOnTestLevelSpec и 2 новые, где сами тесты внутри будут выполняться последовательно:


Еще 2 спецификации для параллельного запуска
class TwoParallelSpec : FreeSpec() {    private companion object {        private val log = LoggerFactory.getLogger(TwoParallelSpec::class.java)    }    init {        "sequential test 1" {            log.info("test 1 started")            delay(1000)            log.info("test 1 finished")        }        "sequential test 2" {            log.info("test 2 started")            delay(1000)            log.info("test 2 finished")        }    }}class ThreeParallelSpec : FreeSpec() {    private companion object {        private val log = LoggerFactory.getLogger(ThreeParallelSpec::class.java)    }    init {        "sequential test 1" {            log.info("test 1 started")            delay(1000)            log.info("test 1 finished")        }        "sequential test 2" {            log.info("test 2 started")            delay(1000)            log.info("test 2 finished")        }    }}

Все 3 спецификации должны запуститься параллельно, а также тесты в OneParallelOnTestLevelSpec выполняться в своих 2-ух потоках:


21:44:16:216 [kotest-engine-1] CustomKotestExtension - [BEFORE] prepareSpec class ru.iopump.qa.sample.parallelism.ThreeParallelSpec21:44:16:216 [kotest-engine-2] CustomKotestExtension - [BEFORE] prepareSpec class ru.iopump.qa.sample.parallelism.TwoParallelSpec21:44:16:216 [kotest-engine-0] CustomKotestExtension - [BEFORE] prepareSpec class ru.iopump.qa.sample.parallelism.OneParallelOnTestLevelSpec21:44:18:448 [SpecRunner-3] ThreeParallelSpec - test 2 started21:44:18:448 [SpecRunner-6] OneParallelOnTestLevelSpec - test 1 started21:44:18:448 [SpecRunner-5] TwoParallelSpec - test 1 started21:44:18:448 [SpecRunner-4] OneParallelOnTestLevelSpec - test 2 started21:44:18:959 [SpecRunner-6] OneParallelOnTestLevelSpec - test 1 finished21:44:19:465 [SpecRunner-5] TwoParallelSpec - test 1 finished21:44:19:465 [SpecRunner-3] ThreeParallelSpec - test 2 finished21:44:19:465 [SpecRunner-4] OneParallelOnTestLevelSpec - test 2 finished21:44:19:471 [SpecRunner-5] TwoParallelSpec - test 2 started21:44:19:472 [SpecRunner-3] ThreeParallelSpec - test 1 started21:44:20:484 [SpecRunner-3] ThreeParallelSpec - test 1 finished21:44:20:484 [SpecRunner-5] TwoParallelSpec - test 2 finished

По логу видно, что все 3 спецификации прошли за 2 сек это сумма последовательных тестов одной их спецификаций выше, тесты из OneParallelOnTestLevelSpec выполнились еще быстрее.
В выводе я оставил первые три строки, чтобы продемонстрировать имена потоков:


  • пул потоков для спецификаций NamedThreadFactory("kotest-engine-%d")
  • пул дочерних потоков для тестов NamedThreadFactory("SpecRunner-%d")

Все это запускается с помощью Executors.newFixedThreadPool и особенности работы с корутинами конкретно в этой конфигурации не используются, т.к. suspend функции выполняются через runBlocking:


executor.submit {    runBlocking {        run(testCase)    }}

Подробности реализации в методах io.kotest.engine.KotestEngine.submitBatch и io.kotest.engine.spec.SpecRunner.runParallel

Исключение из параллельного запуска


Если спецификацию по каким-то причинам следует выполнять в единственном потоке, то есть аннотация @DoNotParallelize.
Для отдельного теста на текущий момент подобного не предусмотрено.


Таймауты


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


Привожу пример спецификации со всеми вариантами таймаута:


Для пояснений в коде я буду использовать метки формата /*номер*/ и после примера рассказывать про каждую в виде: номер -

Варианты описания таймаутов
@ExperimentalTimeclass TimeoutSpec : FreeSpec() {    private companion object {        private val log = LoggerFactory.getLogger(TimeoutSpec::class.java)    }    /*1*/override fun timeout(): Long = 2000 // Spec Level timeout for each TestCase not total    /*2*/override fun invocationTimeout(): Long =        2000 // Spec Level invocation timeout for each TestCase invocation not total    init {        /*3*/timeout = 1500 // Spec Level timeout for each TestCase not total        /*4*/invocationTimeout = 1500 // Spec Level invocation timeout for each TestCase invocation not total        "should be invoked 2 times and will be successful at all".config(            /*5*/invocations = 2,            /*6*/invocationTimeout = 550.milliseconds,            /*7*/timeout = 1100.milliseconds        ) {            log.info("test 1")            delay(500)        }        "should be invoked 2 times and every will fail by invocationTimeout".config(            invocations = 2,            invocationTimeout = 400.milliseconds,            timeout = 1050.milliseconds        ) {            log.info("test 2")            delay(500)        }        "should be invoked 2 times and last will fail by total timeout".config(            invocations = 2,            invocationTimeout = 525.milliseconds,            timeout = 1000.milliseconds        ) {            log.info("test 3")            delay(500)        }    }}

Агрегирующего таймаута на всю спецификацию нет. Все варианты устанавливают таймаут только на тест.

5 invocations = 2 В Kotest есть возможность задать ожидаемое кол-во успешных выполнений одного теста, чтобы считать его успешным.
Например, чтобы проверить требование отсутствия состояния в системы, можно выполнить тест несколько раз, если хотя бы одно выполнение будет неуспешным, то тест будет считаться неуспешным. Не путайте с fluky это обратная ситуация.


1 override fun timeout(): Long = 2000 Через переопределение метода. Установить таймаут на тест в мс.


2 override fun invocationTimeout(): Long = 2000 Через переопределение метода. Установить таймаут на один вызов теста в мс (см 5).


3 timeout = 1500 Через свойство. Установить таймаут на тест в мс и переписать 1


4 invocationTimeout = 1500 Через свойство. Установить таймаут на один вызов теста в мс и переписать 2 (см 5).


6 invocationTimeout = 550.milliseconds Через метод конфигурации теста. Установить таймаут на тест в kotlin.time.Duration и переписать 3.


7 timeout = 1100.milliseconds Через метод конфигурации теста. Установить таймаут на один вызов теста в kotlin.time.Duration и переписать 4 (см 5).


kotlin.time.Duration имеет статус Experimental и при использовании требует установки @ExperimentalTime над классом спецификации

Разберем вывод запуска TimeoutSpec:


08:23:35:183 TimeoutSpec - test 108:23:35:698 TimeoutSpec - test 108:23:36:212 CustomKotestExtension - [AFTER] afterTest. Test case duration: 1047 ms08:23:36:217 TimeoutSpec - test 2Test did not complete within 400msTimeoutException(duration=400)08:23:36:625 TimeoutSpec - test 308:23:37:141 TimeoutSpec - test 3Test did not complete within 1000msTimeoutException(duration=1000)

Первый тест прошел успешно 2 раза, каждое выполнение уместилось invocationTimeout и весь тест в timeout
Второй тест упал на первом выполнении и не стал далее выполняться по invocationTimeout TimeoutException(duration=400)
Третий тест упал на втором выполнении по своему timeout TimeoutException(duration=1000)


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


@ExperimentalTimeobject ProjectConfig : AbstractProjectConfig() {    /** -Dkotest.framework.timeout in ms */    override val timeout = 60.seconds    /** -Dkotest.framework.invocation.timeout in ms */    override val invocationTimeout: Long = 10_000}

Встроенные ожидания


Во время тестов система реагирует на наши действия не мгновенно, особенно в E2E. Поэтому для всех проверок результатов взаимодействия с системой необходимо использовать ожидание. Приведу неполный список основных:


  • ожидание HTTP ответа (уже есть на уровне HTTP клиента, достаточно только указать таймаут)
  • ожидание появления запроса на заглушке
  • ожидание появления/изменения записи в БД
  • ожидание сообщения в очереди
  • ожидание реакции UI (для Web реализовано в Selenide)
  • ожидание письма на SMTP сервере

Если не понятно, почему нужно ждать в заглушке или в БД, отвечу в комментариях или сделаю отдельную публикацию

Есть отличная утилита для тонкой настройки ожиданий: Awaitility о ней пойдет речь в будущих частях. Но Kotest из коробки предоставляет простой функционал ожиданий.
В документации движка этот раздел называется Non-deterministic Testing.


Eventually


Подождать пока блок кода пройдет без Исключений, то есть успешно, в случае неуспеха повторять блок еще раз.
Функция из пакета io.kotest.assertions.timing: suspend fun <T, E : Throwable> eventually(duration: Duration, poll: Duration, exceptionClass: KClass<E>, f: suspend () -> T): T


f это блок кода, который может выбросить Исключение и этот метод будет его перезапускать, пока не выполнит успешно либо не пройдет таймаут duration.
poll это промежуток бездействия между попытками выполнения.
exceptionClass класс исключения по которому нужно попытаться еще раз.
Если есть уверенность, что когда код выбрасывает IllegalStateException, то нужно пробовать еще раз, но если, класс исключения другой, то успешно код точно никогда не выполниться и пытаться еще раз выполнить код не нужно, тогда указываем конкретный класс IllegalStateException::class.


Continually


Выполнять переданный блок в цикле, пока не закончится выделенное время либо пока код не выбросит Исключение.
Функция из пакета io.kotest.assertions.timing: suspend fun <T> continually(duration: Duration, poll: Duration, f: suspend () -> T): T?


f блок кода, который будет запускаться в цикле пока не пройдет таймаут duration либо не будет выброшено Исключение.
poll промежуток бездействия между попытками выполнения


Рассмотрим пример из четырех тестов (2 eventually + 2 continually)


Варианты использования ожидания
@ExperimentalTimeclass WaitSpec : FreeSpec() {    private companion object {        private val log = LoggerFactory.getLogger(WaitSpec::class.java)    }    /*1*/    private lateinit var tries: Iterator<Boolean>    private lateinit var counter: AtomicInteger    private val num: Int get() = counter.incrementAndGet()    init {        /*2*/        beforeTest {            tries = listOf(true, true, false).iterator()            counter = AtomicInteger()        }        "eventually waiting should be success" {            /*3*/eventually(200.milliseconds, 50.milliseconds, IllegalStateException::class) {            log.info("Try #$num")            if (tries.next()) /*4*/ throw IllegalStateException("Try #$counter")        }        }        "eventually waiting should be failed on second try" {            /*5*/shouldThrow<AssertionError> {            eventually(/*6*/100.milliseconds, 50.milliseconds, IllegalStateException::class) {                log.info("Try #$num")                if (tries.next()) throw IllegalStateException("Try #$counter")            }        }.toString().also(log::error)        }        "continually waiting should be success" - {            /*7*/continually(200.milliseconds, 50.milliseconds) {            log.info("Try #$num")        }        }        "continually waiting should be failed on third try" {            /*8*/shouldThrow<IllegalStateException> {            continually(200.milliseconds, 50.milliseconds) {                log.info("Try #$num")                if (tries.next()) throw IllegalStateException("Try #$counter")            }        }.toString().also(log::error)        }    }}

1 Блок с полями для подсчета попыток и описания итератора из трех элементов


2 Перед каждым тестовым контейнером сбрасывать счетчики


3 Вызывается функция eventually, которая завершится успешно. Общий таймаут 200 мс, перерыв между попытками 50 мс, игнорировать исключение IllegalStateException


4 Первые две итерации выбрасывают IllegalStateException, а 3-я завершается успешно.


5 Ожидается, что eventually закончится неуспешно и выполняется проверка выброшенного исключения. При неудаче eventually
выбрасывает AssertionError с информацией о причине неудачи и настройках.


6 таймаута 100 мс и перерыв между попытками 50 мс, позволяют выполнить только 2 неудачный попытки, в итоге ожидание завершается неуспешно


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


8 Ожидается, что continually закончится неуспешно и выполняется проверка выброшенного исключения. При неудаче continually
перебрасывает последнее Исключение от кода внутри, то есть IllegalStateException, чем отличается от eventually


Лог выполнения с пояснениями
/////////////////////////////////////////////////////////////////// 1 ////////////////////////////////////////////////////////////////////////21:12:14:796 INFO CustomKotestExtension - [BEFORE] test eventually waiting should be success21:12:14:812 INFO WaitSpec - Try #121:12:14:875 INFO WaitSpec - Try #221:12:14:940 INFO WaitSpec - Try #3/////////////////////////////////////////////////////////////////// 2 ////////////////////////////////////////////////////////////////////////21:12:14:940 INFO CustomKotestExtension - [BEFORE] test eventually waiting should be failed on second try21:12:14:956 INFO WaitSpec - Try #121:12:15:018 INFO WaitSpec - Try #2/* Сообщение в ошибке содержит информацию о настройках ожидания */21:12:15:081 ERROR WaitSpec - java.lang.AssertionError: Eventually block failed after 100ms; attempted 2 time(s); 50.0ms delay between attempts/* Сообщение в ошибке содержит первое выброшенное кодом Исключение и последнее */ The first error was caused by: Try #1java.lang.IllegalStateException: Try #1The last error was caused by: Try #2java.lang.IllegalStateException: Try #2//////////////////////////////////////////////////////////////////// 3 ///////////////////////////////////////////////////////////////////////21:12:15:081 INFO CustomKotestExtension - [BEFORE] test continually waiting should be success21:12:15:081 INFO WaitSpec - Try #121:12:15:159 INFO WaitSpec - Try #221:12:15:221 INFO WaitSpec - Try #321:12:15:284 INFO WaitSpec - Try #4///////////////////////////////////////////////////////////////////// 4 //////////////////////////////////////////////////////////////////////21:12:15:346 INFO CustomKotestExtension - [BEFORE] test continually waiting should be failed on third try21:12:15:346 INFO WaitSpec - Try #121:12:15:409 INFO WaitSpec - Try #221:12:15:469 INFO WaitSpec - Try #3/* Здесь выбрасывается Исключение из выполняемого блока в continually */21:12:15:469 ERROR WaitSpec - java.lang.IllegalStateException: Try #3

Flaky тесты


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


  • на уровне CI перезапускать всю Задачу прогона тестов, если она неуспешна
  • на уровне Gradle сборки, используя плагин test-retry-gradle-plugin или аналог
  • на уровне Gradle сборки, с помощью своей реализации, например сохранять упавшие тесты и запускать их в другой задаче

Другие инструменты для сборки Java/Kotlin/Groovy кроме Gradle не рассматриваю

  • на уровне тестового движка
  • на уровне блоков кода в тесте (см. раздел Встроенные ожидания и retry)

На текущей момент в версии Kotest 4.3.2 нет функционала для перезапуска нестабильных тестов. И не работает интеграция с Gradle плагином test-retry-gradle-plugin.


Есть возможность перезапустить отдельный блок кода. Это особенно актуально для UI тестирования, когда клик на некоторые элементы иногда не срабатывает.
Представьте ситуацию с Dropdown внизу страницы, после скролла происходит клик, далее ожидание раскрытия элементов и клик на элемент списка.
Так вот, независимо от Фронтового фреймворка, иногда клик для раскрытия списка может не сработать и список не появляется, нужно еще раз попробовать начиная с клика.


Можно сделать повтор через try/catch, но лучше использовать
функцию suspend fun <T, E : Throwable> retry(maxRetry: Int, timeout: Duration, delay: Duration = 1.seconds, multiplier: Int = 1, exceptionClass: KClass<E>, f: suspend () -> T)::


// using Selenidedropdown.scrollTo()retry(2, 20.seconds, exceptionClass = Throwable::class) {    dropdown.click()    dropdownItems.find(Condition.text("1")).apply {        click()        should(Condition.disappear)    }}

Действие scrollTo повторять не нужно. После перехода к элементу блок с раскрытием списка и выбором повторить 2 раза, но не дольше 20 сек игнорируя все исключения (или более конкретные).


В Kotest возможно создать свое расширение для перезапуска упавших спецификаций/контейнеров теста/тестов.
В 3 части руководства я покажу как создать самый простой вариант для уровня тестов.
Остальные варианты я планирую реализовать в своем GitHub репозиторие и выпустить в MavenCentral, если к тому моменту разработчик Kotest не добавит поддержку 'из коробки'.


Фабрики тестов


Представим, что в каждом тесте нужно выполнять одни и те же действия для подготовки окружения: наполнять БД, очереди, конфиги на файловой системе.
Для этих действий хочется создать шаблон теста с адекватным именем, возможно с описанием в виде javadoc/kdoc и несколькими аргументами, например именем и паролем тестового пользователя.
В Kotest такой подход называется Test Factories и позволяет вставлять куски тестов в корневой тест.
Это ни функции, ни методы, ни абстракции это параметризованные части теста с той же структурой, что основной тест, но используемые в нескольких местах кода.


Я придерживаюсь правила, что спецификация теста должна быть понятна любому человеку, имеющему экспертизу в предметной области теста.
Под спецификацией теста я понимаю не реализацию, а описания шагов в виде строк в BDD стиле. В 1-ой части руководства я раскрывал тему форматирования теста более подробно.
Даже если требуется много copy/paste для сохранения формата теста, нужно это делать.
А вот тестовый движок/фреймворк позволяет спецификацию теста запустить, провести взаимодействия с системой автоматически, как если бы все что написано в тесте делал человек.

Очень важно не нарушать читабельность теста использованием шаблонов:


  • имя шаблона должно быть понятное и сопровождаться описанием, а так же описанием всех параметров.
  • шаблон должен выполнять одну функцию, например настройка БД (принцип единственной ответственности)
  • шаблон должен быть описан в BDD стиле
  • шаблон не должен быть слишком абстрактным (субъективно)

Теперь к реализации в Kotest. Она очень ограничена, поэтому я приведу пример того, как рекомендует делать шаблоны официальная документация и как это удобнее делать своими силами через scope-функции и функции-расширения.
Пример кода с пояснениями:


2 вида фабрик тестов. TestFactory и scope-функции
class FactorySpec : FreeSpec() {    init {        /*1.1*/include(containerFactory(1, 2, log))        "root container" - {            /*2.1*/containerTemplate()        }    }}/** Kotest factory */fun containerFactory(argument1: Any, argument2: Any, logger: Logger) =    /*1.2*/freeSpec {    beforeContainer { logger.info("This 'beforeContainer' callback located in the test factory") }    "factory container" - {        "factory test with argument1 = $argument1" { }        "factory test with argument2 = $argument2" { }    }}/** Add [TestType.Container] by scope function extension *//*2.2*/suspend inline fun FreeScope.containerTemplate(): Unit {    "template container with FreeScope context" - {        /*2.3*/testCaseTemplate()    }}/** Add [TestType.Test] by scope function extension *//*2.4*/suspend inline fun FreeScope.testCaseTemplate() {    "nested template testcase with FreeScope context" { }}private val log = LoggerFactory.getLogger(FactorySpec::class.java)

TestFactory

1.1 С помощью метода fun include(factory: TestFactory) включаем шаблон теста в спецификацию в качестве самостоятельного теста.


1.2 Определена функция containerFactory, которая возвращает экземпляр TestFactory и принимает несколько параметров. Для создания объекта TestFactory используем функцию fun freeSpec(block: FreeSpecTestFactoryConfiguration.() -> Unit): TestFactory, которая принимает блок тестов и предоставляет контекст FreeSpecTestFactoryConfiguration.
Внутри этого блока пишем тест, как обычно, в BDD силе. Далее с помощью includeшаблон вставляется как обычный контейнер теста.


Для TestFactory выполняются все обратные вызовы, что определены для спецификации, куда встраивается шаблон.
А также доступны все методы для создания обратных вызовов внутри фабрики, например beforeContainer.
У TestFactory есть большие ограничения:
  • нельзя вставлять include друг в друга
  • встраивать можно только на уровень спецификации, то есть фабрика должна быть полным тестом, а не частью теста.


Шаблоны через scope-функции и функции-расширения.

2.1 Внутри контейнера вызывается пользовательская функция containerTemplate и добавляет к контексту этого контейнера новые шаги теста.


2.2 Функция suspend inline fun FreeScope.containerTemplate(): Unit выполняет свой код в контексте внешнего теста FreeScope и просто изменяет этот контекст, добавляя новый вложенный контейнер.
Функция ничего не возвращает, а именно изменяет (side-effect) переданный контекст. Тесты пишем так, как будто они не в отдельной функции, а в спецификации.
suspend обязателен, т.к. все тесты запускаются в корутине.
inline для скорости, не обязателен. Указывает компилятору на то, что код этой функции нужно просто скопировать вместо вызова, то есть в байт-коде, не будет containerTemplate, а будет вставленный код в спецификации.
FreeScope. внутренности вызываются в контексте этого объекта, в данном случае контейнера


2.3 Внутри шаблона вызывается другой шаблон, который добавлен новые шаги. В реальных тестах так делать не следует


2.4 Функция suspend inline fun FreeScope.testCaseTemplate(): Unit добавляет вложенные шаги теста вместо вызова. Все то же самое, что и 2.2


В итоге имеем следующую структуру теста после выполнения:



Property тестирование


Представим, что мы в 90-х годах на большом и сложном проекте, где критично качество и есть ресурсы на всеобъемлющее тестирование, на всех вариантах тестовых данных и окружений.
В этом случае невозможно использовать ограниченный тестовых набор, если только тип входных данных не ограничен парой вариантов Data-Driver не подходит.
В таких случаях необходимо генерировать тестовые данные случайным образом, либо вообще покрывать абсолютно все возможные значения.
Необходимо не просто уметь генерировать данные, но и легко менять параметры генерации: функции распределения значений, граничные значения и т.п.
Не менее важна возможность повторить последовательность с прошлого прогона, чтобы воспроизвести ошибку. А также требуются инструменты для создания своих генераторов.


Генераторы данных


В Kotest существует 2 типа генераторов данных:


  • Arb (Arbitrary Случайный) генерирует бесконечные последовательности из которых по-умолчанию в тесте будет используется 1000 значений
  • Exhaustive (Исчерпывающий) служит для полного перебора ограниченного набора значений

Полный список генерируемых типов довольно большой и хорошо описан в официальной документации


Основная цель генераторов возможность запускать Property-тесты на их основе. Второстепенная цель генерация единичных значений. Также генераторы предоставляют широкий набор методов по настройке и модификации генерируемой последовательности.


Приведу несколько примеров генераторов для генерации единичных значений или ограниченного набора значений, которые можно с успехом использовать в Data-Driven тестах:


Применение Arb генераторов для генерации случайных данных
/** For string generator with leading zero *//*1*/val numberCodepoint: Arb<Codepoint> = Arb.int(0x0030..0x0039)        .map { Codepoint(it) }/** For english string generator *//*2*/val engCodepoint: Arb<Codepoint> = Arb.int('a'.toInt()..'z'.toInt())    .merge(Arb.int('A'.toInt()..'Z'.toInt()))    .map { Codepoint(it) }class GeneratorSpec : FreeSpec() {    init {        /*3*/"random number supported leading zero" {            Arb.string(10, numberCodepoint).next()                .also(::println)        }        /*4*/"random english string" {            Arb.string(10, engCodepoint).orNull(0.5).next()                .also(::println)        }        /*5*/"random russian mobile number" {            Arb.stringPattern("+7\\(\\d{3}\\)\\d{3}-\\d{2}-\\d{2}").next()                .also(::println)        }        /*6*/"exhaustive collection and enum multiply" {            Exhaustive.ints(1..5).times(Exhaustive.enum<Level>()).values                .also(::println)        }        /*7*/"exhaustive collection and enum merge" {            Exhaustive.ints(1..5).merge(Exhaustive.enum<Level>()).values                .also(::println)        }    }}

1 Класс Codepoint задает набор символов в виде кодов Unicode. Этот класс используется в Arb для генерации строк.
В Kotest есть встроенные наборы символов в файле io.kotest.property.arbitrary.codepoints.
В примере определен набор Unicode цифр (такого набор нет среди встроенных). В дальнейших шагах этот набор будет использован для генерации номеров, которые могут начинаться на 0


2 Определен набор букв английского алфавита. Т.к. в Unicode они идут не по порядку, то сначала создается набор малого регистра, потом merge с набором большого регистра.


3 Использование набора символов цифр numberCodepoint для генерации номеров, где возможен 0 в начале типа String, длина 10 символов.


4 Использование набора символов английского алфавит engCodepoint для генерации слов длиной 10 символов. А также с вероятностью 50% вместо строки сгенерируется null метод orNull(0.5)


5 Метод Arb.stringPattern позволяет генерировать строки на основе RegEx паттерна очень полезная функциональность. Реализация на основе Generex. Производительность оставляет желать лучшего и зависит от сложности регулярного выражения.
Для номера телефона генерация происходит в 10 раз медленнее, чем при использовании Arb.string(10,numberCodepoint) с форматированием. У меня 1000 генераций телефона по паттерну -> 294 мс и 1000 генераций телефона из строки цифр с последующим форматированием -> 32 мс.
Замерять удобно методом measureTimeMillis.


6 Exhaustive фактически просто обертка для перечислимых типов и актуальна только в интеграции с Property-тестами, однако также имеет интересные возможности. Здесь происходит умножение набора Int на объекты enum Level и на выходы получается 25 Pair. Если интегрировать в тест, то будет 25 запусков для каждой пары.


7 Exhaustive объединение в одну коллекцию из 10 разнородных элементов вида: [1, ERROR, 2, WARN ...]


Вся функциональность генераторов доступна не только для Property-тестов, но и для простых тестов, где нужны случайные значения тестовых данных.
Для генерации одного значения используется терминальный метод Arb.next

Результаты запуска:


// Test 12198463900// Test 2tcMPeaTeXG// Test 3+7(670)792-05-16// Test 4[(1, ERROR), (1, WARN), (1, INFO), (1, DEBUG), (1, TRACE), (2, ERROR), (2, WARN), (2, INFO), (2, DEBUG), (2, TRACE), (3, ERROR), (3, WARN), (3, INFO), (3, DEBUG), (3, TRACE), (4, ERROR), (4, WARN), (4, INFO), (4, DEBUG), (4, TRACE), (5, ERROR), (5, WARN), (5, INFO), (5, DEBUG), (5, TRACE)]// Test 5[1, ERROR, 2, WARN, 3, INFO, 4, DEBUG, 5, TRACE]

Написание и конфигурирование Property тестов


Kotest предоставляет две функции для запуска Property-тестов, а также их перегруженные вариации:


  • suspend inline fun <reified A> forAll(crossinline property: PropertyContext.(A) -> Boolean)
  • suspend inline fun <reified A> checkAll(noinline property: suspend PropertyContext.(A) -> Unit)

Отличие между ними заключается в том, что переданный блок в forAll должен возвращать Boolean, а в checkAll все проверки должны быть внутри, что более привычно.


Выполнить 1000 итераций кода со случайным входным числом типа Long:


checkAll<Long> { long: Long ->    val attempt = this.attempts()    println("#$attempt - $long")    long.shouldNotBeNull()}

Обращаю внимание на то, что функциональное выражение в аргументе forAll и checkAll не простое, а с дополнительным контекстом!


suspend PropertyContext.(A) -> Unit имеем аргумент generic типа (в примере это long: Long), но также имеем доступ к контексту PropertyContext, через ключевое слово this (для красоты его можно опустить).


val attempt = this.attempts() здесь возвращается кол-во пройденных попыток из PropertyContext, но там есть и другие полезные методы.


Пользовательские генераторы

У обоих видов генераторов (Arb и Exhaustive) есть терминальные методы forAll и checkAll, которые запускают блок теста, принимающий сгенерированные значения от пользовательских генераторов в качестве аргумента.


Arb

Задача: Рассмотрим написание Property-теста на примере основной теоремы арифметики: каждое натуральное число можно представить в виде произведения простых чисел. Также это называется факторизация.
Допустим имеется реализованный на Kotlin алгоритм разложения и его необходимо протестировать на достаточном наборе данных.
Код алгоритма можно найти в примерах


Тестовый сценарий: сгенерированное число, разложить с помощью алгоритма на простые множители и проверить, что полученные множители простые и их произведение равно исходному числу тем самым мы проверим реализацию алгоритма.
Очевидно, что одного прогона недостаточно и 10 прогонов недостаточно, как минимум нужно прогнать тест на 1000 случайных величин, а может и больше.


Параметры генератора данных: Кол-во = 1000. Сгенерированное число должно быть больше 1. В сгенерированной последовательности первые два числа должны быть граничные: 2 и 2147483647.


"Basic theorem of arithmetic. Any number can be factorized to list of prime" {    /*1*/Arb.int(2..Int.MAX_VALUE).withEdgecases(2, Int.MAX_VALUE).forAll(1000) { number ->    val primeFactors = number.primeFactors    println("#${attempts()} Source number '$number' = $primeFactors")    /*2*/primeFactors.all(Int::isPrime) && primeFactors.reduce(Int::times) == number}    /*3*/Arb.int(2..Int.MAX_VALUE).checkAll(1000) { number ->    val primeFactors = number.primeFactors    println("#${attempts()} Source number '$number' = $primeFactors")    /*4*/primeFactors.onEach { it.isPrime.shouldBeTrue() }.reduce(Int::times) shouldBe number}}

1 создается генератор на 1000 итераций, множество значений от 2 до Int.MAX_VALUE включительно, отдельным методом withEdgecasesнеобходимо явно указать 2 и Int.MAX_VALUE граничными значениями.
Методом forAll запускаем тестирование, выходной результат блока теста Boolean. AssertionError Движок выбросит за нас, если будет результат false.


2 метод all принимает ссылку на метод и проверяет, что каждый множитель простой, метод reduce выполняет умножение, а также выполняется конъюнкцию двух проверок.


3 все абсолютно аналогично 1. Отличие только в том, что блок кода не должен возвращать Boolean и используются стандартные Assertions, как в обычном тесте.


4 вместо логических операций используем проверки Kotest


Exhaustive

Для исчерпывающего тестирования все аналогично Arb подходу.


Допустим, реализован метод, который в зависимости от enum UUIDVersion генерирует указанный тип UUID. А также же метод должен принимать null и генерировать UUID типа UUIDVersion.ANY.
Сигнатура: fun UUIDVersion?.generateUuid(): UUID


Чтобы проверить этот функционал, нужно перебрать абсолютно все значения UUIDVersion + null.
Не имея знаний о возможностях Exhaustive можно просто перебрать данные в цикле. Однако Exhaustive упрощает и без того несложную задачу:


"UUIDVersion should be matched with regexp" {    /*1*/Exhaustive.enum<UUIDVersion>().andNull().checkAll { uuidVersion ->    /*2*/uuidVersion.generateUuid().toString()    /*3*/.shouldBeUUID(uuidVersion ?: UUIDVersion.ANY)    .also { println("${attempts()} $uuidVersion: $it") }}}

1 reified функция Exhaustive.enum создаст последовательность всех значений UUIDVersion и добавит null, а далее будет вызван Property-тест


2 вызов тестируемой функции-расширения для генерации UUID


3 встроенная в Kotest проверка на соответствие UUID регулярному выражению


Генераторы по-умолчанию

Если использовать функции forAll и checkAll без явного указания генератора типа Arb или Exhaustive, то будет использоваться генератор по-умолчанию в зависимости от Generic-типа.
Например для forAll<String>{ } будет использован генератор Arb.string(). Вот полный набор из внутренностей Kotest:


fun <A> defaultForClass(kClass: KClass<*>): Arb<A>? {    return when (kClass.bestName()) {        "java.lang.String", "kotlin.String", "String" -> Arb.string() as Arb<A>        "java.lang.Character", "kotlin.Char", "Char" -> Arb.char() as Arb<A>        "java.lang.Long", "kotlin.Long", "Long" -> Arb.long() as Arb<A>        "java.lang.Integer", "kotlin.Int", "Int" -> Arb.int() as Arb<A>        "java.lang.Short", "kotlin.Short", "Short" -> Arb.short() as Arb<A>        "java.lang.Byte", "kotlin.Byte", "Byte" -> Arb.byte() as Arb<A>        "java.lang.Double", "kotlin.Double", "Double" -> Arb.double() as Arb<A>        "java.lang.Float", "kotlin.Float", "Float" -> Arb.float() as Arb<A>        "java.lang.Boolean", "kotlin.Boolean", "Boolean" -> Arb.bool() as Arb<A>        else -> null    }}

Примеры кода:


"check 1000 Long numbers" {    checkAll<Long> { long ->        long.shouldNotBeNull()    }}

Seed

Последнее, что хотелось бы добавить про Property-тестирование и генераторы, это воспроизведение ошибки. Для воспроизведения необходимо повторить тестовые данные. Чтобы повторить последовательность, существует такое понятие, как seed.
Используемый алгоритм псевдо случайных чисел для генерации последовательности использует некий порождающий элемент (seed), и для равных seed получаются равные последовательности.
Cемя изначально генерируется случайно, используя внешний порождающий элемент, например, время либо физический процесс. При возникновении ошибки, Kotest печатает используемое семя, которое можно указать в конфигурации теста и воспроизвести результат.


"print seed on fail" {    /*1*/shouldThrow<AssertionError> {    checkAll<Int> { number ->        println("#${attempts()} $number")        /*2*/number.shouldBeGreaterThanOrEqual(0)    }}./*3*/message.shouldContain("Repeat this test by using seed -?\\d+".toRegex())}"test with seed will generate the same sequence" {    Arb.int().checkAll(/*4*/ PropTestConfig(1234567890)) { number ->        /*5*/if (attempts() == 24) number shouldBe 196548668        if (attempts() == 428) number shouldBe -601350461        if (attempts() == 866) number shouldBe 1742824805    }}

1 Ожидаем исключение AssertionError с семенем.


2 Из 1000 чисел последовательности примерно половина будет точно меньше 0 и проверка должна сработать почти сразу


3 Демонстрация, что сообщение исключения содержит информацию о числе seed


4 Для теста явно указано семя в конструкторе PropTestConfig. Ожидаем, что для этого теста будет генерироваться всегда одна последовательность, независимо от платформы


5 attempts() возвращает номер итерации. Для итераций 24, 428, 866 будут всегда сгенерированы одинаковые числа


Заключение


Во-первых, привожу ссылку на все примеры qa-kotest-articles/kotest-second.


Был рассмотрен практически весь функционал Kotest


Осталось рассказать про создание расширений и доступные встроенные расширения, а далее перейдем к интеграции с другими библиотеками помогающими в автоматизации тестирования


Ресурсы


Kotlin. Автоматизация тестирования (часть 1). Kotest: Начало


Примеры кода


Официальная документация Kotest


Kotest GitHub


Kotlin Lang


Coroutines Tutorial


Gradle Testing

Подробнее..

Язык тестовых сценариев Testo Lang простая автоматизация сложных тестов

12.10.2020 08:12:38 | Автор: admin

Картинка для привлечения внимания


Если Вы разрабатываете более-менее сложный программный продукт, то Вам должна быть знакома ситуация, когда системные (end-to-end) тесты по тем или иным причинам автоматизировать не удаётся. На это могут быть разные причины, я приведу несколько примеров:


  • У приложения нет и не может быть API, за которое можно зацепиться, по соображениям безопасности;
  • Приходится поддерживать legacy-проект, про автоматизацию тестирования которого никто никогда не задумывался;
  • Во время тестирования задействуется сторонний продукт, например антивирус;
  • Необходимо проверить работоспособность продукта на большом количестве различных целевых платформ;
  • Тестовый стенд представляет собой сложную гетерогенную систему, включающую в себя промежуточное сетевое оборудование.

Эти и многие другие ситуации приводят к худшему кошмару любого разработчика ручному тестированию. Самое неприятное заключается в том, что нельзя провести тестирование один раз и забыть о нём. Нет, нам приходится перед каждым релизом (а может и чаще) раскатывать виртуалки, устанавливать туда тестируемое приложение и тыкать по кнопкам снова и снова, чтобы убедиться, что мы не словили регрессию.


Если Вы ищете решение этой проблемы то прошу под кат.


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


кликнуть_мышкой_на_кнопке_с_надписью "Сохранить"напечатать_на_клавиатуре "Hello world"дождаться_надписи_на_экране "Готово"

При этом неважно, что именно вы тестируете: XAML-приложение, Qt-приложение, Electron-приложение, веб-страницу или вообще консольное приложение. Вы кликаете по экрану виртуальной машины и набираете текст на клавиатуре, а как приложение устроено внутри это Вас уже совершенно не волнует. Удобно? Конечно!


Одна только загвоздка довольно трудно понять, где на экране виртуалки находится кнопка "Сохранить", и есть ли на экране надпись "Готово". Я думаю, это одна из причин, почему мы не видим на рынке переизбытка инструментов, работающих по описанному принципу.


Однако, в последнее время технологии компьютерного зрения шагнули далеко вперёд. Искусственные нейронные сети успешно справляются с такими сложными задачами, как, например, управление автомобилем. Неужели они не справятся с такой заведомо более простой задачей, как обнаружение элементов GUI на экране виртуальной машины?


Как вы могли уже догадаться, нейросетям такая задача вполне по плечу. Вы можете сами в этом убедится, попробовав в действии новый инструмент для автоматизации системных тестов интерпретатор тестовых сценариев Testo. Интерпретатор Testo позволяет выполнять сценарии, написанные на специализированном языке тестовых сценариев Testo Lang, которые выглядят примерно следующим образом:


mouse click "Сохранить"type "Hello world"wait "Готово"

Это всё, что Вам надо написать на языке Testo Lang чтобы:


  1. Кликнуть на надпись на экране "Сохранить";
  2. Напечатать на клавиатуре "Hello world";
  3. Дождаться появления на экране строки "Готово".

Однако, мне бы не хотелось, чтобы у Вас сложилось ложное впечатление, будто бы Testo это какой-то аналог AutoIt или Sikuli. Нет, это не просто инструмент для автоматизации чего-то-там, это инструмент, заточенный именно под автоматизацию системных тестов. Testo берёт под свой контроль значительное количество задач, которые обычно выполняет тестировщик: определение того, какие тесты пора прогнать заново, подготовку виртуального стенда, составление отчёта о том, какие тесты свалились и в какой момент, и так далее.


Так, с этого момента поподробнее



Итак, мы в Вами говорим именно о системных (end-to-end) тестах. Системные тесты предполагают, что Вы тестируете программу не саму по себе в вакууме, а помещаете её в конкретное окружение и смотрите, как она с этим окружением справится. Под окружением может пониматься что угодно: и версия ОС, и наличие/отсутствие каких-то приложений/драйверов, и взаимодействие по сети, и соединение с Интернетом, и недостаток дискового пространства/оперативной памяти Да и много чего ещё.


Самый удобный способ создать такое окружение для программы это установить тестируемую программу внутрь виртуальной машины. Но виртуальную машину необходимо для начала создать и установить на неё операционную систему. Давайте посмотрим, как с этой задачей справляется платформа Testo. Мы предусмотрели специальные конструкции в языке тестовых сценариев Testo Lang, предназначенные для создания элементов виртуальной инфраструктуры Ваших стендов. Например, следующий сниппет объявляет "пустую" виртуалку:


machine my_super_vm {    ram: 2Gb    cpus: 2    iso: "ubuntu_server.iso"    disk main: {        size: 5Gb    }}

Эта конструкция создаёт виртуальную машину с 2Гб оперативной памяти, 2 ядрами процессора и 5Гб дискового пространства. При запуске такой виртуалки, начнётся процесс установки операционной системы из образа ubuntu_server.iso.


Это может быть несколько непривычно, но мы рассматриваем процесс установки операционной системы как ещё один тест, наравне с теми тестами, в которых проверяется собственно работоспособность Вашей программы. Это утверждение обретёт бОльший смысл, если мы на секунду представим, что мы разрабатываем не программу, а операционную систему. Может быть это какая-то специализированная система, например Alt Linux, а может быть мы разрабатываем игрушечную операционную систему just for fun. В любом случае, тестировать её как-то надо, а платформа Testo подходит для этой цели как нельзя лучше, потому что для неё нет никакой разницы, что мы тестируем: операционную систему или программу.


Так а что же делать с пустой виртуалкой? В качестве примера давайте посмотрим, как мог бы выглядеть тест, написанный на языке Testo Lang и выполняющий установку операционной системы:


test my_super_test {    my_super_vm {        start        wait "Language"        press Enter        wait "Install Ubuntu Server"        press Enter        wait "Choose the language"        press Enter        # И так далее        ...    }}

Здесь мы видим новую конструкцию языка, которая объявляет тест my_super_test. В этом тесте учавствует всего одна виртуалка my_super_vm. Тест начинается с включения виртуальной машины. Затем мы дожидаемся, когда на экране появится надпись "Language" и нажимаем клавишу Enter. Собственно, весь тест будет заключаться в последовательности таких действий: ждём наступления события, затем печатаем что-то на клавиатуре.


Разумеется, далеко не всегда хочется заморачиваться с установкой ОС и её первичной настройкой. Поэтому мы предусмотрели возможность импорта диска от другой виртуальной машины:


machine my_super_vm {    ram: 2Gb    cpus: 2    disk main: {        source: "prepared_vm.qcow2"    }}

То есть вы можете вручную подготовить виртуальную машину, установить туда ОС, установить дополнительные программы, выполнить какие-то настройки, например, отключить фаервол, а затем получившийся диск от вручную созданной виртуалки использовать как начальное состояние для виртуалок из автотестов.


Подготовка виртуалки и установка ОС это конечно всё очень хорошо, но тесты, тесты-то на мою программу где? Хорошо, давайте представим, что мы хотим протестировать инсталлятор нашей супер-программы. Представим также, что мы уже вручную подготовили виртуальную машину с Windows 10 на борту. Для простоты примера предположим, что инсталлятор нашей супер-программы уже скопирован на рабочий стол этой виртуалки. Тогда автотест на установку программы будет выглядеть следующим образом:


machine my_win_vm {    ram: 2Gb    cpus: 2    disk main: {        source: "my_windows_10.qcow2"    }}test my_installer_test {    my_win_vm {        # Запустим виртуальную машину        start        # Дождёмся появления рабочего стола        wait "Корзина"        mouse dclick "my_super_installator"        wait "Добро пожаловать"        mouse click "Далее"        wait "Выберите путь установки"        mouse click "Продолжить"        wait "Успешно" timeout 10m        mouse click "Закрыть"    }}

Правда, просто? А мы только разогреваемся ...


Что за wait такой и как он работает?



Сделаем мини перерыв и поговорим пару минут о том, как это вообще работает. Тестовые сценарии состоят по большей части из двух вещей:


  1. Воздействие на виртуальную машину (mouse move/click, type, press, start/stop, plug flash и много чего ещё);
  2. Анализ происходящего на экране (wait).

Так вот, это действие wait и является основным видом визуальных проверок в языке Testo Lang. Действие wait дожидается появления на экране определённых объектов и событий в течение заданного таймаута (по умолчанию одна минута). И если событие не наступило генерируется ошибка (прямо как человек, который ждёт надписи "Успешно" пока у него не кончится, наконец, терпение).


Если мы говорим про поиск текста на экране виртуалки (то бишь на скриншоте), то обычно для этих целей используют какую-нибудь OCR (Optical Character Recognition) систему (например, Tesseract). Однако, это не совсем верный подход. Дело в том, что OCR-системы строятся исходя из двух постулатов:


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

В случае автотестов мы имеем совершенно иную ситуацию:


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

В платформе Testo для реализации этой задачи мы использовали нейросети. На вход нейросетям мы подаём сразу две вещи: скриншот экрана виртуалки и искомый текст. От нейросети требуется сказать только, есть ли что-то похожее на изображении или нет. Например, если мы ищём слово "Hopa", то нам вполне подойдёт как слово, написанное кириллицей, так и слово, написанное латиницей, потому что выглядят они совершенно одинаково.


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


На одном wait далеко не уедешь


Писать длинные тесты с помощью wait + click довольно муторно, особенно, если нет автоматического рекодера тестов. Тесты на основе визуальных проверок это скорее крайний вариант, когда нет другой возможности протестировать приложение или настроить тестовое окружение. Обычно всё же существует возможность выполнить какие-то проверки путём запуска процессов на гостевой системе, например с помощью bash-скриптов.


Как раз на этот случай мы предусмотрели специальную конструкцию в языке Testo Lang. Для её работы на виртуальной машине должны быть установлены дополнения для гостевых систем, которые постовляются вместе с интерпретатором Testo. После установки гостевых дополнений становится возможным писать такие тесты:


test my_super_test  {        my_super_vm {            exec bash "echo Hello world from bash"            exec python """                print("Hello from python")            """        }}

Если какая-либо команда из bash-скрипта завершится с ошибкой, то и весь тест будет считаться проваленным. Аналогично, если python-скрипт вернёт не ноль, то выполнение теста сразу же завершится.


На самом деле довольно многие тестовые сценарии выглядят следующим образом. Сначала с помощью wait + click на гостевую систему устанавливаются дополнения, а затем проверки сводятся к запуску процессов на виртуалке. Но при этом ничто не мешает в любой момент вернуться к визуальным проверкам. Тут всё зависит от Вас как Вам удобнее, так и делайте.


Кроме того, гостевые дополнения позволяют легко копировать файлы между виртуалкой и хостом:


test copy_demo {        my_super_vm {            copyto "/file/on/host" "/file/on/guest"            copyfrom "/file/on/guest" "/file/on/host"        }}

Да зачем же придумывать целый язык?



Мне кажется, многие читатели сейчас думают: "Ребят, серьёзно? Целый язык? Зачем? Ну напишите Вы библиотеку для питона или для чего ещё. Все разумные люди так делают".


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


  1. Мы бы хотели, чтобы у нашего языка был минимальный порог вхождения для людей, не знакомых с программированием;
  2. Мы бы хотели избавиться от лишней мишуры, присущей языкам общего назначения, оставив только то, что требуется для автотестов;
  3. Некоторые фичи, которые мы запилили в Testo-lang, просто так не воткнешь в библиотеку для Python!

Например, Testo поддерживает кеширование тестов, благодаря чему тесты можно прогонять инкрементально только в случае, когда это необходимо (аналогично инкрементальной компиляции программ).


Допустим, у Вас есть такой тест:


test run_installator {        my_super_vm {            copyto "/path/on/host.msi" "C:\\Users\\Testo\\Desktop\\setup.msi"            mouse dclick "setup"            ...        }}

Допустим, Вы его запустили, и он прогнался успешно. Если Вы тут же запустите его ещё раз он отработает мгновенно! Так а действительно, зачем прогонять тест ещё раз, если:


  1. Сам тест не менялся;
  2. Сборка Вашего инсталлятора не менялась.

А вот как только Вы соберёте новую сборку своего инсталлятора и запустите тест ещё раз то он запустится заново! Testo отслеживает все внешние файлы, участвующие в тестах, и как только в них что-то меняется, кеш соответствующих тестов сбрасывается. И да, этот механизм полностью прозрачный Вам не надо писать ничего для того, чтобы это работало.


Ух, круто. А что ещё умеет Testo?



Одна виртуалка в тестах это скучно. Самое веселье начинается, когда виртуалок становится много, и они начинают взаимодействовать между собой. В тестовых сценариях Вы можете создавать сколько угодно виртуалок и соединять их сетями (можно и доступ в Интернет добавить):


# Сеть для соединения двух виртуальных машинnetwork net1 {    mode: "internal"}# Сеть для доступа в Итнернетnetwork internet {    mode: "nat"}machine my_super_client {    ...    nic server_side: {        attached_to: "net1"    }    nic internet: {        attached_to: "internet"    }}machine my_super_server {    ...    nic client_side: {        attached_to: "net1"    }}

Хотите добавить в стенд флешку? Нет проблем, пара строк и у Вас есть виртуальная флешка (можно даже скопировать на неё что-нибудь с хоста)


flash my_super_flash {    fs: ntfs    size: 2048Mb    #Папка с хоста, которую надо скопировать    folder: "/path/on/host"}

Хотите написать реально много тестов? Нет проблем, организуйте их в иерархию! Давайте для большей конкретики рассмотрим такой набор тестов:


  1. Установка ОС;
  2. Установка гостевых дополнений;
  3. Копирование и установка тестируемой программы на виртуалку;
  4. Тестирование фичи 1;
  5. Тестирование фичи 2.

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



На языке Testo Lang это будет выглядеть не сильно сложнее, чем на рисунке:


test install_os {    ...}test install_guest_additions: install_os {    ...}test install_app: install_guest_additions {    ...}test test_feature_1: install_app {    ...}test test_feature_2: install_app {    ...}

При первом запуске тестов, конечно, все тесты прогонятся от начала до конца. Допустим, при этом запуске не произошло никаких ошибок. В этом случае Testo запомнит, что все эти тесты завершились успешно и, соответственно, закешированы:



Если прямо сейчас заново запустить тесты, то они вовсе не прогонятся, так как ничего с момента последнего запуска и не поменялось. Но как только Вы соберёте новый билд своей тестирумой программы, Testo это отловит и инвалидирует кеш 3, 4 и 5 тестов:



При этом тест с установкой ОС и гостевых дополнений останутся закешированными в них ведь ничего не поменялось, так ведь? ОС та же самая, гостевые дополнения и текст тестового сценария тоже. Поэтому эти тесты прогоняться заново не будут. Вместо этого Testo откатит виртуалки, которые учавствуют в тестах, к тому состоянию, в котором они пребывали в конце теста install_guest_additions.


Простой, но вполне реальный пример


Возможностей Testo может хватить на десяток статей, поэтому нам бы не хотелось сейчас Вам рассказывать про все из них. Давайте лучше притормозим и рассмотрим базовый, но реальный пример с автоматизацией тестирования простенького самописного standalone-приложения MySuperApp.


Это приложение написано на С++ с использованием библиотеки ImGui, у него нет никаких хуков для автоматизации тестирования, но мы всё равно очень хотим на каждую сборку проверять, что оно успешно запускается на Windows 10 и высвечивает нам окошко с надписью "MySuperApp is working!".


Что ж, для начала нам понадобится виртуалка. Для этого примера мы создадим виртуалку на основе уже существующей, вручную подготовленной виртуалки, на которую мы заранее установили Windows 10:


machine my_vm {    cpus: 2    ram: 4Gb    disk main: {        source: "${QEMU_DISK_DIR}/win10.qcow2"    }}

А как нам скопировать на виртуалку сборку с нашей программой? Давайте для этого воспользуемся флешкой!


flash my_super_flash {    fs: "ntfs"    size: 16Mb    folder: "./dist"}

Сборку с нашей программой поместим в каталог "./dist", а Testo позаботится о том, чтобы она оказалась на флешке.


Ну а теперь пишем сам тест!


test launch_my_simple_app {    my_vm {        ...    }}

Так, стоп, а с чего начать? Да всё просто просто записывайте все действия, которые Вы бы проделывали вручную! Для начала виртуалку надо включить.


start

Окей, а дальше? Дальше надо дождаться появления рабочего стола, конечно же:


wait "Recycle Bin" timeout 10m


Вставляем флешку


plug flash my_super_flash


Кликаем по надписи "USB Drive (E:)"


mouse click "USB Drive (E:)"


Открываем файловый менеджер:


mouse click "Open folder to view files"


Дважды кликаем по нашему приложению:


mouse dclick "MySuperApp"


Как же нам понять, что наше приложение успешно запустилось? Ну, мы знаем, что наше приложение при запуске должно высвечивать надпись "hello world". Поэтому если такая надпись появилась на экране, то это с большой долей вероятности свидетельствует о том, что всё хорошо. Это и будет наша основная проверка в тесте:


wait "hello world"

В конце теста не забываем вытащить флешку, и в сумме у нас получается такой скрипт:


test launch_my_simple_app {    my_vm {        start        wait "Recycle Bin" timeout 10m        plug flash my_super_flash        mouse click "USB Drive (E:)"        mouse click "Open folder to view files"        mouse dclick "MySuperApp"        wait "hello world"        unplug flash my_super_flash    }}

Вот собственно и всё, наш первый тест готов. А как его запустить? Да тоже ничего сложного, главное указать путь QEMU_DISK_DIR:


sudo testo run my_script.testo --param QEMU_DISK_DIR /var/lib/libvirt/qemu/images

Мы подготовили небольшой видеоролик, на котором выполняется запуск этого теста:



А теперь, напоследок, давайте попробуем симитировать ситуацию, когда в нашу программу закралась ошибка.


Например, по какому-то нелепому стечению обстоятельств, мы собрали MySuperApp с динамической линковкой стандартной библиотеки С++ вместо статической. Если программа собрана таким образом, то для её работы на гостевой системе должен быть установлен пакет Microsoft Visual C++ Redistributable. А мы разрабатываем standalone-приложение, которое не должно иметь никаких зависимостей. У разработчика на хостовой системе Microsoft Visual C++ Redistributable конечно же установлен, поэтому такую ошибку легко не заметить.


Итак, мы подкладываем в каталог ./dist новую сборку нашего приложения и запускаем тесты заново. Вот что мы увидим:



При этом в выводе интерпретатора Testo будет указано, какой тест свалился и в какой именно строчке тестового сценария это произошло:



Тест свалился, ошибка выловлена!


Итоги


Системные тесты это всегда непросто, но всегда очень ценно. Мы надеемся, что с Testo Вам будет максимально комфортно тестировать свои программы в самых разных условиях, и Вы сможете спать спокойно, зная, что теперь у заказчика Ваша программа будет себя вести так, как и задумывалось.


Скачать Testo абсолютно бесплатно без регистрации и СМС, а также ознакомиться с документацией можно у нас на сайте https://testo-lang.ru


Посмотреть больше примеров можно на youtube-канале https://www.youtube.com/channel/UC4voSBtFRjRE4V1gzMZoZuA

Подробнее..

Я автоматизировал тестирование Dr. Web. А сможете ли вы?

15.10.2020 14:05:55 | Автор: admin


Я никогда не пользовался Dr. Web. Я понятия не имею, как он устроен. Но это не помешало мне написать для него ряд автотестов (и лишь лень не позволила мне написать ещё сотню других):


  1. Тест на установку Dr. Web;
  2. Тест на ограничение доступа к съемным устройствам (флешкам);
  3. Тест на разграничение доступа к каталогу между программами;
  4. Тест на разграничение доступа к каталогу между пользователями системы (родительский контроль).

Такие и многие другие тесты можно клепать как горячие пирожки, и не только применительно к Dr. Web, и не только применительно к антивирусам. В этой статье я расскажу, как это сделать.


Подготовка


Для тестов нам понадобится виртуалка с Windows на борту. Я подготовил её вручную, выполнив на ней следующие манипуляции:


  1. Собственно, установил Windows 10 Pro x64;
  2. Во время установки создал основного пользователя "testo" в паролем "1111";
  3. Включил автологин для этого пользователя;

Для автоматизации тестов я буду использовать платформу Testo. Что это такое и как этим пользоваться можно почитать здесь. Нам же сейчас требуется импортировать готовую виртуалку в автотесты. Сделать это очень просто:



Здесь предполагается, что /path/to/win10.qcow2 это путь к диску той виртуалки, которую я подготовил вручную. На этом подготовка заканчивается, и начинается экшен.


Тест 1 Устанавливаем Dr. Web!


Для начала надо решить вопрос с переносом дистрибутива Dr. Web на виртуальную машину. Сделать это можно (например) с помощью флешки:



Всё, что нам надо сделать это положить установщик Dr. Web в папочку ${DR_WEB_DIR} (точное значение этого параметра мы будем задавать при запуске testo). А Testo само позаботится о том, чтобы этот инсталлятор оказался на флешке.


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



Скриншот на момент окончания сценария


Можно, конечно, запустить инсталлятор прямо отсюда, из самой флешки. Но мы лучше сделаем всё по-честному мы скопируем инсталлятор на рабочий стол и запустим инсталлятор оттуда. Как же нам скопировать файл? А как бы это сделал человек?



Скриншот, на котором ещё происходит копирование файла


Всё, копирование успешно завершено! Теперь можно закрыть окно с флешкой и вытащить её:



Скриншот после закрытия проводника


Теперь, когда инсталлятор оказался на рабочем столе, нам нужно дважды кликнуть на него чтобы запустить процесс установки. А сама установка сводится к простому прокликиванию кнопок и галочек и не представляет большого интереса:



Скриншот на момент окончания установки


Завершаем наш тест перезагрузкой. И в конце не забудем проверить, что после перезагрузки на рабочем столе появилась иконка с Dr. Web:



Скриншот после перезагрузки


Отличная работа! Мы автоматизировали установку антивируса Dr. Web! Давайте немного передохнём и посмотрим, как это выглядит в динамике:



Переходим к тестированию фич.


Тест 2 Ограничение доступа к флешкам


Первая фича по списку ограничение доступа к флешкам. Для этого спланируем довольно прямолинейный тест:


  1. Попробуем вставить флешку и создать там пустой файл должно получиться. Вытащим флешку;
  2. Включим блокировку съемных устройств в Dr. Web Security Center;
  3. Ещё раз вставим флешку и попробуем удалить созданный файл. Действие должно быть заблокировано.

Создадим себе новую флешку, вставим её в Windows и попробуем создать папку. Что может быть проще?



Скриншот на момент окончания сценария


Создаём новый текстовый файл через контекстное меню проводника:



Скриншот после переименования файла


Отключаем флешку, делаем это безопасно:



Теперь мы убедились, что с флешкой работать можно, а значит можно приступать к её блокировке в центре безопасности Dr. Web. Для этого сначала надо открыть центр безопасности:



Скриншот с окном Security Center


Можем заметить, что для открытия любого приложения в Windows нужно выполнить фактически одинаковые действия (кликнуть на строку поиска, дождаться появления окна с популярными приложениями, вбить имя интересующего приложения, дождаться, когда оно появится в списке и, наконец, нажать Enter). Поэтому эту группу действий можно выделить в макрос open_app, в который в качестве параметра будет передаваться имя открываемого приложения:



Этот макрос нам ещё пригодится.


Первое, что мы сделаем, открыв центр безопасности Dr. Web включим возможность вносить изменения:



Теперь немного покликаем по менюшкам и зайдем в меню "Configure device access rules". В этом меню поставим галочку в пункте "Block removable media".



Скриншот с окном Devices and Personal Data


Попробуем открыть флешку теперь:



Скриншот с сообщение об ошибке


Вот так потихоньку-полегоньку мы и написали первый тест с тестированием вполне осязаемой фичи в Dr. Web. Настало время передохнуть и помедитировать, глядя на результаты наших трудов:




Тест 3 Разграничение доступа к каталогу между программами


Основная идея этого тесткейса проверить работу Dr. Web при ограничении доступа к определенной папке. Если конкретно, то необходимо защитить папку от каких-либо изменений, но добавить исключение для какой-нибудь сторонней программы. Собственно, сам тест выглядит следующим образом:


  1. Установим на ОС стороннюю программу, для которой чуть позже добавим исключение при доступе к защищаемой папке. Сегодня сторонняя программа дня файловый менеджер FreeCommander;
  2. Создаем папку с файликом, которую будем защищать всеми силами;
  3. Откроем центр безопасности Dr. Web и включим там защиту этой папки;
  4. Настроим исключение для FreeCommander;
  5. Попробуем удалить файл из защищаемой папки обычным способом (через проводник Windows). Не должно получиться;
  6. Попробуем удалить файлик через FreeCommander. Должно получиться.

Ух, много работы. Быстрее начнём быстрее закончим.


Пункт первый, установка FreeCommander не сильно отличается от установки Dr.Web. Обычная рутина: вставили флешку, запустили инсталлятор и так далее. Пропустим это и перейдём сразу к интересному.


Если всё-таки интересно, как установить FreeCommander

Начнём с простого: создадим флешку, в которую поместим дистрибутив FreeCommander, а затем в тесте вставим флешку в ОС и откроем её:



Далее несколько не кликов чтобы запустить установку:



Установка не очень интересная, просто кликаем везде "Далее", а в конце не забываем отключить галочки с просмотром ReadMe и немедленным запуском FreeCommander



Заканчиваем тест, закрывая все окна и вытаскивая флешку



Готово!


Для работы с Dr. Web создадим новый тест dr_web_restrict_program, который будет полагаться на результат работы предыдущего теста win10_install_freecommander.


Начнём тест с создания папки Protected на рабочем столе:



Скриншот после создания папки


Заходим в папку Protected и создаём там файл my_file.txt, который будет играть роль защищаемого файла:



Ох, надо было бы тоже оформить это в виде макроса, ну да ладно ...


Скриншот после создания файла


Отлично, теперь надо включить защиту папки. Идём знакомой дорожкой и открываем Dr. Web, не забываем включить режим изменений. После чего переходим в меню "Data Loss Prevention".



Скриншот с окном Data Loss Prevention


Немного поработаем мышкой и добавим нашу папку Protected в список защищаемых:



Скриншот с мастером добавления защищаемой папки


Ну а теперь надо настроить исключение по доступу к папке для FreeCommander. Ещё немного работы мышкой:



Скриншот с добавленной программой-исключением


Теперь аккуратно закрываем все окна и пробуем удалить файл "my_file.txt" стандартным способом:



Скриншот с сообщением от Dr.Web


Но ничего не получилось значит Dr. Web действительно отработал! Половина теста позади, но нам ещё надо проверить, что будет работать исключение для FreeCommander. Для этого открываем FreeCommander и переходим в папку Protected:



Скриншот с окном FreeCommander


Ну и попробуем удалить файл my_file.txt:



Скриншот после удаления файла


Исключение для FreeCommander работает!


Отличная работа! Большой и сложный тесткейс и всё автоматизировано. Немного расслабона:




Тест 4 Родительский контроль


Этот последний на сегодня тесткейс мы построим следующим образом:


  1. Создадим нового пользователя MySuperUser;
  2. Залогинимся под этим пользователем;
  3. Создадим файл my_file.txt от имени нового пользователя;
  4. Откроем центр безопасности Dr. Web и включим родительский контроль для этого файла;
  5. В родительском контроле ограничим права пользователя MySuperUser на им же созданный файл;
  6. Попробуем прочитать и удалить файл my_file.txt от имени MySuperUser и посмотрим на результат.

Я не буду приводить здесь сценарий теста. Он строится по тому же принципу, что и предыдущие тесты: активно работаем мышкой и клавиатурой. При этом нам не важно, что мы автоматизируем хоть Dr.Web, хоть создание нового пользователя в Windows. Но давайте всё же посмотрим, как будем выглядеть прогон такого теста:



Заключение


Исходники всех тестов Вы можете посмотреть здесь


Более того, все эти тесты Вы можете прогнать на своей машине. Для этого Вам потребуется интерпретатор тестовых сценариев Testo. Скачать его можно здесь.


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

Подробнее..

С чего начинаются тесты

21.10.2020 08:16:04 | Автор: admin

Тестов много не бывает. И речь идёт не только о наращивании их количества (что само по себе, конечно, тоже хорошо) речь идёт о разнообразии самих видов тестов. Даже не напрягая воображение можно вспомнить несколько способов протестировать ваше приложение: Unit-тесты, интеграционные тесты, API-тесты, системные тесты и это не вспоминая о том, что тесты ещё бывают функциональными, нагрузочными, направленными на отказоустойчивость...


Но с чего же начинать писать тесты для новых проектов? Лично для меня, как для программиста, самый интуитивный ответ это Unit-тесты. Однако опрометчиво накидываться на сочинение Unit-тестов может не только оказаться бесполезым занятием, но даже нанести вред в будущей разработке проекта.


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


А можно всех посмотреть?


Я думаю, многие из вас видели ту или иную версию вот этой картинки:



Существуют различные вариации этой картинки может меняться количество слоёв, их название, состав, но суть остаётся неизменной: тестирование программ можно разбить на несколько уровней, и при этом чем выше мы поднимаемся по этим уровням тем дороже и сложнее становится разработка и проведение тестов и, соответственно, количество тестов на уровне N+1 будет неизменно меньше, чем на уровне N.


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


Unit-тесты


В основе пирамиды лежат Unit-тесты (или, в русском варианте, модульные тесты). Unit-тесты направлены на тестирование небольших модулей программы. Зачастую под модулем понимается отдельный класс или даже функция.



Несколько особенностей Unit-тестов:


  1. Подразумевают принцип "белого ящика" то есть эти тесты требуют наличия и понимания исходных кодов программы.
  2. Дёшево стоят, можно наклепать тысячи, если не десятки тысяч Unit-тестов.
  3. Быстро прогоняются.
  4. По своей природе автоматизированы.
  5. Позволяют очень точно локализовывать ошибки можно узнать конкретную функцию/класс, которая работает неправильно.
  6. Плохо подходят для комплексных проверок.

Интеграционные тесты


На следующем уровне пирамиды обычно указываются интеграционные тесты. Интеграционные тесты направлены на тестирование больших обособленных блоков программы и их взаимодействие между собой (опционально). Под блоком может пониматься отдельная библиотека с частью функционала программы, или же просто большой обособленный участок кода, который слишком велик для покрытия обычными Unit-тестами. Если же программа не бьётся на обособленные блоки и имеет монолитную архитектуру, то интеграционные тесты могут прогоняться для всей программы целиком, но при этом программа помещается в "вакуум" никак не общается и не взаимодействует с "внешним миром".



Несколько особенностей интеграционных тестов:


  1. Всё ещё требуют принцип "белого ящика" тестирование блоков подразумевает понимание интерфейса этих блоков, как минимум. Внутреннее устройство модулей в интеграционном тестировании уже не участвует.
  2. Несколько сложнее Unit-тестов, т.к. зачастую блоки требуют много подготовительной работы, прежде чем они смогут "нормально" функционировать.
  3. Чуть сложнее поддаются автоматизации всё зависит от конкретного блока и его устройства/назначения.

API-тесты


Также в пирамиде иногда выделяют API-тесты. Иногда их относят к интеграционным, иногда как отдельный уровень, но сути это не меняет. Если какая-то часть программы имеет чётко выраженный и оформленный API, то можно выстроить тесты, "дергая" этот API и сверяя фактический результат с ожидаемым. Часто такое можно встретить в распределённых приложениях.


Сейчас такой вид тестов получил просто огромное распространение за счёт очень большой популярности REST API. Это очень удобный способ связывать клиент-серверные приложения между собой, и поэтому, конечно, создаётся очень заманчивое желание абстрагироваться от клиента и протестировать только серверную составляющую, дергая за "ниточки" API.


Несколько свойств этого вида тестирования:


  1. Может рассматриваться как подвид интеграционного тестирования.
  2. Требует чётко оформленного и (желательно) документированного API.
  3. Чаще всего нормально поддаётся автоматизации. Если речь идёт о популярном виде API (например, REST) то к вашим услугам большое количество готовых открытых и коммерческих инструментов, которые позволяют автоматизировать такие тесты на раз-два. Если же API нетипичное, то может потребоваться разработать собственную утилиту по вызову этого API и проверке результатов. В любом случае, стоимость автоматизации выше, чем у Unit-тестов.
  4. Позволяет протестировать очень большие самостоятельные компоненты программы и иногда всю программу в целом.

Тем не менее, нужно заметить, что этот вид тестирования возможен только при наличии у программы (или у её компонентов) чётко сформированного API. Что бывает далеко не всегда (например, по соображениям безопасности, или просто наличие API не предусмотрено архитектурой приложения).


Системные тесты


На вершине пирамиды располагаются системные (или, как их иногда называют, end-to-end) тесты. Иногда на вершине пирамиды указывают UI-тесты, но лично я бы не стал выделять этот вид тестов в отдельный уровень. Впрочем, это лишь дело вкуса, потому что UI-тесты можно считать подмножеством системных тестов.



Системные тесты направлены на тестирование всей программы в целом, с учётом окружения, в которой программе предстоит работать. Это уточнение очень важное, ведь программа редко когда может работать "сама по себе". Программа работает на одном или нескольких (в случае распределенных приложений) конкретных компьютерах. И на разных компьютерах (в разных условиях) тестируемая программа может повести себя по-разному. Например, клиент-серверное приложение может работать замечательно в обычной сети, но при этом может оказаться неработоспособным при наличии между клиентом и сервером маршрутизатора с настроенным NAT.


Такой вид тестов является самым дорогим, самым сложным и самым трудноавматизируемым видом тестов. Зачастую системные тесты настолько сложно автоматизировать, что их и вовсе прогоняют вручную, что ещё больше увеличивает их стоимость. При этом этот вид тестов обладает одним неоспоримым и важнейшим преимуществом: это тестирование программы в том виде, в котором её увидит пользователь. Больше того, системные тесты позволяют даже воспроизвести условия различных пользователей, чтобы убедиться, что у всех пользователей всё работает хорошо.


Несколько заметок о системных тестах:


  1. Предполагают тестирование по методу "черного ящика". Никаких знаний об исходных кодах или особенностях работы программы только UI-тесты.
  2. Наиболее комплексный вид тестирования даже распределенные приложения тестируются вместе, а не по отдельности.
  3. Даёт наибольшую степень уверенности, что протестированные фичи действительно будут работать у конечных пользователей.
  4. Самый дорогой вид тестов.
  5. Долго или очень долго прогоняются.

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


Что ж, ликбез в теорию я заканчиваю, и перехожу к самой интересной части какой вид тестов лучше писать в первую очередь?


Не спешите с Unit-тестами


Я думаю, что рисунок с пирамидой тестов создаёт обманчивое впечатление. Этот рисунок призван лишь продемонстрировать распределение количества тестов по разным уровням в зависимости от их стоимости. Но вместо этого он создаёт ощущение, что тесты более высокого уровня как бы "базируются" на тестах более низкого уровня. И, соответственно, может показаться, что "пока не разберусь с Unit-тестами, об интеграционных и говорить нечего. Ну а до системных хорошо если вообще доберёмся". И я считаю, что это в корне неверная позиция.


Представим ситуацию, что вы разрабатываете совершенно новое приложение. Неважно, как именно вы его разрабатываете на основе четко сформированных требований или же "по наитию". В какой-то момент времени вам (надеюсь) захочется написать тесты. Это может случиться как только у вас получится что-то работающее, или же тесты окажутся вообще вашим самым первым шагом если вы придерживаетесь концепции TDD (Test Driven Development). Неважно когда, но этот момент настанет. И о каких же тестах вы подумаете в первую очередь?


Лично я, как разработчик, всегда думаю в первую очередь о Unit-тестах! А почему бы и нет? Ведь для меня программа это, в первую очередь, исходный код. Если я сам пишу код и знаю, как он работает/должен работать, то для меня самый логичный первый шаг покрыть его тестами. Больше того, я уже знаю, как это делается, я делал это тысячу раз, это зона моего комфорта. Поэтому я с чувством выполнения великой миссии (тесты это же хорошо!) начинаю стандартную возню с Unit-тестами: скачиваю свой любимый фреймворк, продумываю абстрактные классы вместе с интерфейсами, занимаюсь mock-ированием объектов И радуюсь, какой я молодец.


И тут я загоняю себя в две потенциально-опасные ситуации. Посмотрим на первую проблему.


Кто может быть первым кандидатом на Unit-тесты? Лично я бы начал с обособленных участков кода и классов, которые выглядят вполне самостоятельными. Например, я решил, что для моего проекта мне понадобится самописная хеш-таблица. Это же отличный кандидат на покрытие Unit-тестами! Интерфейс понятен и меняться не будет, так что написать тесты сам бог велел. И вот я воодушевлённо трачу своё рабочее время, накидывая десятки тестов. Может быть, я даже выловлю несколько багов в своём коде (боже, как я хорош, как мощны мои тесты) и и спустя два месяца я вдруг понимаю, что хеш-таблица мне вовсе не нужна, лучше бы её заменить базой данных. И все мои рабочие часы на Unit-тесты (и выловленные баги) летят в помойку.


Обидно? Ну что ж, с кем не бывает, ничего страшного. Это ведь не повод отказываться от Unit-тестов, верно? Сделаем заметку и рассмотрим вторую опасную ситуацию.


Отловив все очевидные участки кода, которые можно покрыть Unit-тестами, вы вдруг понимаете, что вы покрыли всего 10% кода (ну нет у вас сейчас четких обособленных модулей с внятными интерфейсами на такой стадии проекта). Тогда вы начинаете беспощадно рефакторить свой код выделяете абстрактные классы, выстраиваете зону ответственности между модулями, инкапсулируете, инкапсулируете, инкапсулируете занимаетесь, в общем-то, полезными делами, честно говоря. Ну и каждый свой успех отмечаете очередной порцией Unit-тестов, ведь ради них, родимых, всё и затевается!


Спустя пару недель работы вы получаете 60% покрытого тестами кода. У вас появилась сложная иерархия классов, mock-объекты, а общее количество Unit-тестов перевалило за 100500. Всё хорошо, так ведь?


Всё хорошо ровно до тех пор, пока вы не увидите, что вашу архитектуру можно было бы улучшить (а с развитием проектов, особенно новых, такие наблюдения случаются регулярно). И вместо того, чтобы смело ринуться в дебри рефакторинга, вы начинаете грустно думать о 100500 Unit-тестах и о том, как теперь вам придётся их переделывать. Вы становитесь заложником своих собственных Unit-тестов, которые начинают сковывать вашу гибкость в принятии архитектурных решений. И выбор у вас получается так себе. Либо решиться на рефакторинг и спустить в унитаз всё время, потраченное на Unit-тесты, либо (ещё хуже) оставить всё как есть, и затем бороться с не самой удачной (как вы теперь понимаете) архитектурой.


Звучит так, как будто я просто персонально терпеть не могу Unit-тесты и пытаюсь отговорить вас от их использования, так?


Вовсе нет. Просто для любого нового проекта это абсолютно нормальная ситуация в какой-то момент понять, что изначальная архитектура или затея вышла не слишком удачной и что нужно всё (или практически всё) переделывать. Вам всё равно с высокой долей вероятности придётся через это пройти. И наличие Unit-тестов вас не только не спасёт оно даже сыграет с вами злую шутку: вместо того, чтобы взвешенно и объективно принять решение о реструктуризации кода, вы будете только грустно вспоминать о потраченных часах и даже днях на составление сотен, тысяч юнит-тестов. Мне было бы жалко времени, потраченного на Unit-тесты, которые теперь надо выкинуть в помойку и начинать писать заново.


Лучше спешите с системными тестами!


Так что же я предлагаю? А предлагаю я взглянуть на старый рисунок по-новому:



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


Как я уже упоминал выше, системные тесты это тесты программы в целом. При этом программа представляется именно в том виде, в котором её увидит пользователь. Фактически, системные тесты заставляют нас взглянуть на программу как на законченный продукт (хоть он таким сейчас и не является). Если другими словами, то вместо подхода от "от частного к общему" мы начинаем писать тесты "от общего к частному". А это позволяет нам убить сразу двух зайцев:


  1. Прорабатывая системные тесты, мы формализуем и фиксируем требования к программе в виде скриптов, которые или проходят или нет. Соответственно, будет гораздо меньше споров о том, когда фичу можно считать законченной.
  2. Системные тесты позволяют очень четко визуализировать и разложить по полочкам то, как наша программа будет выглядеть для конечного пользователя. Это уже ценно само по себе, но также позволяет обнаружить на ранних этапах несостыковки в требованиях (а они всегда есть). Также становится понятным, какие моменты требуют дальнейшей проработки.

Звучит неплохо, не правда ли? Причём, системные тесты обладают ещё рядом очень приятных бонусов:


  1. В условиях зафиксированных требований к программе системные тесты не могут сильно меняться. Это возможно благодаря самой природе системных тестов: они рассматривают программу как "черный ящик" без какой-либо привязки к архитектуре и особенностям реализации. А это значит, что вы можете свободно менять архитектуру своей программы по первому требованию, и никакие тесты менять не придется! Ну, разве что, немного подкорректировать если у вас добавилась новая зависимость или что-то в таком духе.
  2. Системные тесты могут писать аналитики или даже тестировщики не отнимая, таким образом, ценное время программистов. Также это позволяет аналитикам дать программистам более четкое понимание, что именно они хотят увидеть в программе. Программистам лишь останется добиться того, чтобы тесты проходили, не ломая голову над вопросом "чего же от меня хотят".
  3. Системные тесты это очень комплексный вид тестов. Если ваша программа проходит сложный комплексный тест то вы уже с довольно высокой долей вероятности можете быть уверены, что "в целом, наверное, все компоненты нормально работают". В Unit-тестах наоборот уверенность в одном классе не дает абсолютно никакой уверенности в работоспособности программы в целом.

Остаётся только одна проблема: системные тесты с трудом поддаются автоматизации. Возможно, это одна из причин, почему сейчас системные автотесты или оставляют "на потом", или вообще ограничиваются ручным тестированием.


Впрочем, сейчас на этом фронте не всё так уж и плохо существует очень много коммерческих решений, которые позволяют в том или ином виде решить эту проблему: Testcomplete, Squish, Ranorex, Eggplant это лишь самые известные примеры таких систем. У всех у них есть свои достоинства и недостатки, но в целом со своей задачей по автоматизации системных тестов они справляются (хоть и стоят очень немалых денег).


А вот среди бесплатных решений выбора особо нет. По крайней мере не было.


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


А что же другие тесты?


Заметьте, что я сосредоточился на том, что стоит начинать с системных тестов, потому что это даёт определенные неплохие бонусы. Но это отнюдь не означает, что стоит пренебрегать другими видами тестов.


В самом деле, системные тесты дают неплохое представление о том, что программа в целом работает, но при этом они не ограничивают вашу свободу при работе скальпелем в вашем проекте. Но это не отменяет того факта, что системные тесты всё равно остаются довольно тяжеловесными (даже с учётом автоматизации) и их действительно невозможно клепать с той же легкостью и скоростью, как API-тесты или, тем более, Unit-тесты.


В развитием проекта вы обнаружите, что вас уже не устраивает только проверка "в целом всё работает". Со временем архитектура вашего нового приложения неизбежно "устаканится", крупных изменений будет всё меньше и меньше. В какой-то момент вы поймёте, что вы хотите навесить побольше проверок на какой-нибудь компонент (потому что он уже точно никуда из проекта не денется, и его интерфейс точно проработан), или даже на конкретный класс А для особо важных участков кода и вовсе желательно проработать все возможные ветвления. Или же вам очень интересно, как поведёт себя программа при непосредственном обращении к API. Чувствуете, да? Вот и другие тесты снова в деле!


На основе всего вышеизложенного, я бы хотел предложить следующий вариант развития тестов в проекте:


  1. В самом начале, когда проект активного развивается с нулевого состояния, необходимо писать только системные тесты. Тестов нужно написать столько, чтобы у вас была четкая уверенность, что "в целом моя программа работает нормально". Это будет отличный базис для дальнейшей работы.
  2. По мере развития проекта и "устаканивания" его архитектуры и основных компонентов можно добавлять интеграционные тесты. Если у проекта появилось чётко выраженное и стабильное API нужно начинать писать API-тесты.
  3. Наконец, для особо важных изолированных участков кода, от правильной работы которых зависит очень многое, можно написать Unit-тесты.
  4. Помнить, что из любого правила есть исключения. При возникновении достаточно веских объективных причин повышайте приоритет интеграционных и Unit-тестов. Не нужно откладывать разработку тестов более низкого уровня, если вы в них действительно нуждаетесь здесь и сейчас.

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


Итоги


Вроде бы про разработку "от общего к частному" уже давным давно всё известно вдоль и поперёк, но при этом тесты всё равно упорно составляются "от частного к общему". Возможно, это дело привычки, а возможно в том, что так просто удобнее ведь написать двадцать Unit-тестов всегда проще, чем пару системных тестов.


Но по мере развития инструментов для автоматизации системных тестов перспектива сходу автоматизировать end-to-end тесты уже не кажется такой уж пугающей. Возможно, вам понравится разрабатывать тесты "сверху вниз" ведь так их можно разрабатывать ровно по мере их надобности.

Подробнее..

Почему большинство юнит тестов пустая трата времени? (перевод статьи)

17.05.2021 16:18:04 | Автор: admin

Автор: James O Coplien

Перевод: Епишев Александр

1.1 Наши дни

Во времена FORTRAN, когда функция была функцией, иногда заслуживающей функциональных проверок, юнит-тестирование было одним из главных составляющих. Компьютеры производили вычисления, в то время как функции и процедуры представляли собой вычислительные блоки. В те времена доминирующий подход в дизайне предполагал создание комплексной внешней функциональности из более мелких кусков, которые, в свою очередь управляли еще более мелкими, и так далее, вплоть до уровня хорошо понятных примитивов. Каждый слой поддерживал находящийся над ним слой. В целом, у вас были большие шансы отследить, как функциональность на самом дне, так называемые функции и процедуры, были связаны с требованиями, выраженными в доступном человеку интерфейсе. Можно было рассчитывать, что хороший дизайнер поймет бизнес цель той или иной функции. Такими же возможными для понимания были и взаимосвязи в дереве вызовов, как минимум в хорошо структурированном коде. Вы могли мысленно смоделировать выполнение кода во время код-ревью.

Постепенно объектное ориентирование штурмом захватывает мир, а мир проектирования, и вовсе, переворачивает вверх дном. Во-первых, проектируемые блоки превратились из вычислительных единиц в маленькие гетерогенные композиты, называемые объектами и совмещающие в себе несколько программных артефактов, включая и функции, и данные внутри одной обертки. Объектная парадигма использовала классы для оборачивания нескольких функций вместе с общими для этих функций спецификациями данных. Подобный класс становился шаблоном, из которого в ран-тайме создавались объекты. В таком вычислительном контексте, необходимая функция определяется во время выполнения и не может быть вызвана из исходного кода, как это было возможно с FORTRAN. Это привело к невозможности разобраться во взаимосвязях поведения кода в ран-тайме лишь за счет анализа. Теперь вам необходимо запустить программу для того, чтобы получить малейшее представление о том, что же в ней происходит.

Итак, тестирование снова вступает в игру. На этот раз - юнит-тестирование на стероидах. Объектно-ориентированное сообщество обнаружило ценность ранней обратной связи, обусловленной возрастающей скоростью техники, а также появлением всё большего числа персональных компьютеров. Дизайн становится более сфокусированным на данных, так как объекты всё больше конструируются исходя из структур этих данных, чем каких-либо свойств своих же методов. Нехватка явно выраженной структуры вызовов усложнила возможность поместить выполнение какой-либо атомарной функции в контекст своего же выполнения. И насколько бы малым ни был шанс для подобной возможности, и тот был отнят полиморфизмом. В результате, интеграционное тестирование оказывается вне, а первоочередную роль принимает на себя юнит-тестирование. Где-то на фоне всё ещё виднелось системное, но, скорее уже как проблема кого-то другого, или, что еще более опасно, как один из вариантов более зрелой версии юнит-тестирования, которое проводилось самими же создателями кода.

Классы превратились в объекты анализа, и, в определенной степени, проектирования. Популярной техникой дизайна стали CRC-карты (обычно представляющие Классы, Ответственности и Кооперацию), каждый класс в которых представлен отдельным человеком. Объектная ориентация начала ассоциироваться с антропоморфным дизайном. Классы, к тому же, превратились в единицы администрирования, дизайна и программирования, а их антропоморфная суть усилила стремление каждого создателя класса его протестировать. Поскольку у некоторых методов класса сохранилась такая же контекстуализация, как и у функции FORTRAN, у программистов возникла необходимость предоставлять контекст перед выполнением метода (помните, что мы не тестируем классы и, даже, не тестируем тестовые объекты, единицей функционального теста является метод). Юнит тесты обеспечивали выполнение сценариев драйверами. Моки - контекст состояния окружения (энва) и других методов, от которых зависел тестируемый метод. При подготовке к тесту, тестовые окружения поставляли необходимые средства для создания каждого объекта в его правильном состоянии.

1.2 Лекарство хуже болезни

Конечно же, юнит-тестирование не является проблемой исключительно объектно-ориентированного программирования, de rigueur (лат. "крайней необходимостью"), скорее всего, его сделала комбинация объектной-ориентированности, эджайла, разработки программного обеспечения, а также рост инструментов и вычислительных мощностей. Как консультант, я часто слышу вопросы о юнит-тестировании, включая следующий от одного из своих клиентов, Ричарда Якобса (Richard Jacobs) из Sogeti (Sogeti Nederland B.V.):

Второй вопрос касается юнит-тестов. Если я правильно припоминаю, вы говорили, что юнит-тесты - это пустая трата времени. Во-первых, я был удивлен. Тем не менее, сегодня моя команда сообщила, что их тесты сложнее, чем сам код. (Это не та команда, которая изначально написала код и юнит тесты. Поэтому некоторые тесты застают их врасплох. Текущая команда более высоко квалифицирована и дисциплинирована.) В таком случае, по моему, это пустая трата времени... Когда я ежедневно программировал, то создавал действительно тестируемый код, однако, почти никогда не писал никакие юнит тесты. При этом я заслуживал признание за свой качественный код и почти безошибочное программное обеспечение. Мне хотелось бы разобраться, ПОЧЕМУ такой вариант работал в моем случае?

Вспомните из своего университетского образования, что любую программу можно смоделировать как ленту Тьюринга, и то, на что именно способна эта программа, каким-то образом связано с количеством битов на данной ленте перед её выполнением. Если вы хотите тщательно протестировать такую программу, вам понадобится тест, по крайней мере, с таким же объемом информации: то есть другая лента Тьюринга, как минимум, с таким же количеством бит.

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

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

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

1.3 Тесты ради тестов и спроектированные тесты

У меня был клиент из Северной Европы, разработчики которого должны были предоставить 40% покрытия кода, для, так называемого, 1-го уровня зрелости программного обеспечения, 60% для 2-го уровня и 80% для 3-го, хотя были и стремящиеся к 100%. Без проблем! Как вы могли бы предположить, достаточно сложная процедура с ветвлениями и циклами стала бы вызовом, однако, это всего лишь вопрос принципа divide et impera (разделяй и властвуй). Большие функции, для которых 80% покрытие было невозможным, разбивались на множество более мелких, для которых 80% уже было тривиальным. Такой подход повысил общий корпоративный показатель зрелости команд всего лишь за один год, потому как вы обязательно получаете то, что поощряете. Конечно же, это также означало, что функции больше не инкапсулировали алгоритмы. Невозможным оказалось понимание контекста выполняемой строки, точнее тех, которые предшествуют и следуют за ней во время выполнения, поскольку эти строки кода больше не имеют прямого отношения к той, которая нас интересует. Такой переход в последовательности теперь происходил благодаря вызову полиморфной функции - гипер-галактической GOTO. Даже если всё, что вас беспокоит, - это покрытие решений (branch coverage), это больше не имеет значения.

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

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

Задумайтесь на секунду о вычислительной сложности этой задачи. Под 100% покрытием, я подразумеваю проверку всех возможных комбинаций всех возможных ветвлений, проходящих через все методы класса, которые воспроизводят все возможные конфигурации битов данных, доступные этим методам, в каждой инструкции машинного языка во время выполнения программы. Все остальное - это эвристика, о корректности которой нельзя сделать никаких формальных заявлений. Число возможных путей выполнения с помощью функции невелико: скажем, 10. Перекрестное произведение этих путей с возможными конфигурациями состояний всех глобальных данных (включая данные экземпляра, которые для области видимости метода являются глобальными) и формальных параметров в действительности же очень велико. Перекрестное произведение этого числа с возможной последовательностью методов внутри класса представляется счетно-бесконечным. Если вы возьмете несколько типичных чисел, то быстро осознаете, насколько вам повезло, если получите покрытие лучше, чем 1 из 1012.

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

Если вы пишете тест с целью покрытия как можно большего количества возможных сценариев, тогда можете уже выделять ферму машин для прогона тестов и отслеживания последних результатов 24/7.

Помните, однако, что автоматизированный хлам - это всё ещё хлам. И те из вас, у кого есть корпоративная Lean-программа, могли заметить, что основы производственной системы Toyota, которые лежали в основе Scrum, очень сильно противились автоматизации интеллектуальных задач (http://personeltest.ru/away/www.computer.org/portal/web/buildyourcareer/Agile Careers/-/blogs/autonomation). Более эффективно - это постоянно удерживать человека процессе, что становится еще более очевидным при исследовательском тестировании. Если вы собираетесь что-то автоматизировать, автоматизируйте что-нибудь ценное. Автоматизировать необходимо рутинные вещи. Возможно даже, вы получите еще больше прибыли от инвестиций, если автоматизируете интеграционные тесты, тесты для проверки регрессионных багов, а также системные, вместо того, чтобы заниматься автоматизацией юнит тестов.

Более разумный подход уменьшает объем тестового кода за счет формального проектирования тестов: то есть, формальной проверки граничных условий, большего количества тестов белого-ящика и т.д. Для этого необходимо, чтобы программный юнит проектировался как тестируемый. Вот как это делают инженеры по аппаратному обеспечению: разработчики предоставляют контрольные точки, способные считывать значения c J-Tag микросхем, для доступа к внутренним значениям сигналов микросхем - это равносильно доступу к значениям между промежуточными вычислениями, содержащимися в вычислительном юните. Я настоятельно рекомендую делать подобное на системном уровне, на котором должно быть сосредоточено основное внимание тестирования; я никогда не видел, чтобы кто-то достигал подобного на уровне юнита. Без таких приемов вы ограничиваете себя юнит-тестированием черного ящика.

Я мог бы поверить в формализованный дизайн юнит теста, если бы такое поведение можно было формализовать, то есть, если бы существовал некий абсолютный, формальный оракул корректности, на основе которого можно было бы создать тест. Подробнее об этом ниже. В противном случае это лишь догадка программиста.

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

1.4 Убеждение, что тесты умнее кода, говорит о скрытом страхе или плохом процессе

У программистов существует негласное убеждение о том, что они способны мыслить более ясно (или, скорее, догадываться), во время создания тестов, чем при написании исходного кода, или что в тесте, каким-то образом, содержится больше информации, чем в самом коде. Это просто формальная ерунда. Психология здесь поучительна и важна, потому что именно она, а не какое-то рассчитываемое свойство, больше всего руководит поведением разработчика.

Если ваши кодеры предоставляют в юнит-тестах больше строк, чем в исходном коде, это, вероятно, означает одно из следующего. У них может быть параноидальное отношение к корректности; паранойя вытесняет ясное мышление и инновации, которые служат предзнаменованием высокого качества. Им может не хватать аналитических ментальных инструментов или дисциплины мышления, и хотелось бы, чтобы машина думала за них. Машины хорошо справляются с повторением механических задач, однако дизайн тестов по-прежнему требует тщательного обдумывания. Или может случиться так, что в вашем процессе частая интеграция невозможна из-за плохо налаженных процессов разработки или неподходящих инструментов. Программисты делают всё возможное, чтобы компенсировать подобную ситуацию за счет тестов там, где они хотя бы в какой-то степени обретают контроль над собственной судьбой.

Если у вас большой объем юнит-тестов, оцените обратную связь в процессе разработки. Интегрируйте код чаще; сократите время сборки и интеграции; сократите количество юнит тестирования и перейдите больше к интеграционному.

Проблема также может быть в другом: разработчики не обладают достаточно развитыми навыками проектирования, или же процесс не стимулирует архитектурное мышление и добросовестный подход к дизайну. Возможно, требования настолько плохи, что разработчики даже и не поняли бы что нужно протестировать, если бы им пришлось это сделать, поэтому всё, чем они руководствуются - это их лучшие догадки. Исследования программной инженерии показали, что наиболее экономически эффективным для исправления ошибок является момент перехода от анализа к проектированию, сам дизайн, а также дисциплина кодирования. Намного проще избежать ошибок, чем их устранять.

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

Тем не менее, будем честны, ошибки будут всегда. Тестирование никуда не денется.

1.5 У тестов с низким уровнем риска низкая (даже потенциально отрицательная) отдача

Как то я озвучил своему клиенту предположение о том, что множество их тестов могут быть тавтологическими. Предположим, задача какой-то функции - это присвоение X значения 5, и я готов поспорить, что существует соответствующий тест для данной функции, который, после запуска, проверяет, равняется ли X 5. Снова же, хорошее тестирование, основывается на тщательном размышлении, а также базовых принципах управления рисками. Управление рисками строится на статистике и теории информации; если тестировщики (или, по крайней мере, менеджер по тестированию) не обладают хотя бы элементарными навыками в этой области, вы, с большой вероятностью, создаете множество бесполезных тестов.

Разберем тривиальный пример. Цель тестирования - предоставить информацию о вашей программе. (Тестирование само по себе не повышает качество; это делают программирование и проектирование. Тестирование лишь сообщает об упущениях команды в создании правильного проектирования и соответствующей реализации.) Большинство программистов хотят услышать информацию о том, что их программный компонент работает. Поэтому, как только в проекте трехлетней давности была создана первая функция, тут же для нее был написан и юнит тест. Тест ни разу не падал. Вопрос: Много ли информации содержится в этом тесте? Другими словами, если 1 - это успешно выполненный тест, а 0 - упавший, тогда сколько будет информации в следующей строке результатов:

11111111111111111111111111111111

Существует несколько возможных ответов, обусловленных видом применяемого формализма, хотя большинство из них не верны. Наивный ответ - 32, однако, это биты данных, а не информации. Возможно, вы информационный теоретик и скажете, что количество битов информации в однородной двоичной строке равносильно двоичному логарифму длины этой строки, которая в данном случае равна 5. Однако это не то, что я хочу знать: в конце концов хотелось бы понять, сколько информации можно получить после одноразового прогона такого теста. Информация основывается на вероятности. Если вероятность успешного прохождения теста равняется 100%, тогда, по определению теории информации, этой информации нет вообще. Ни в одной из единиц указанной выше строки не содержится почти никакой информации. (Если бы строка была бесконечно длинной, то в каждом тестовом прогоне было бы ровно ноль битов информации.)

Далее, сколько бит информации в следующей строке тестовых прогонов?

1011011000110101101000110101101

Ответ... намного больше. Вероятно, 32. Это означает, что в каждом тесте содержится намного больше информации. Если мы изначально не способны предсказать, пройдет ли тест успешно или нет, тогда каждый запуск теста содержит полный бит информации, и добиться чего-то лучшего вы не сможете. Видите ли, разработчики любят поддерживать тесты, которые проходят успешно, потому что это подогревает их эго и уровень комфорта. Однако, информация поступает от упавших тестов. (Конечно же, мы могли бы взять другую крайность:

00000000000000000000000000000000

в которой, фактически, нет никакой информации, в том числе, даже о процессе улучшения качества.)

Если вы хотите сократить объем тестов, первое, что следует сделать, - это обратить внимание на те, которые ни разу за год не упали, и рассмотреть вопрос об их удалении. Такие тесты не предоставляют вам никакой информации, или, по крайней мере, очень мало информации. Ценность производимой ими информации может не оправдывать затрат на поддержку и выполнение таких тестов. Эти тесты - первые претенденты для удаления, и не имеет значения, это юнит тесты, интеграционные или системные.

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

Не стоит недооценивать интеллект своих людей, как и не стоит недооценивать коллективную глупость множества совместно работающих людей в сложном домене. Возможно, вы подумали, что никогда не допустили бы ошибок описанной выше команды, но я всегда нахожу все больше и больше таких случаев, в которые сложно поверить. Скорее всего и у вас в шкафу есть какие-то из этих скелетов. Отыщите их, посмейтесь над собой, исправьте их и двигайтесь дальше.

Если у вас есть подобные тесты - это второй претендент на удаление.

Третий набор для удаления - тавтологические тесты. Я сталкиваюсь с ними чаще, чем вы можете себе представить, особенно среди последователей, так называемой, разработки через тестирование (TDD). (Кстати, проверка this на ненулевое/не пустое (non-null) значение при входе в метод, не является тавтологической, и может быть очень информативной. Однако, как и в случае с большинством юнит тестов, лучше сделать ассершн, чем пичкать свой тестовый фреймворк подобными проверками. Подробнее об этом ниже.)

Во многих компаниях, единственные тесты с бизнес-ценностью - это те, в основании которых лежат бизнес-требования. Большинство же юнит тестов основываются на фантазиях программистов о том, как должна работать функция: на их надеждах, стереотипах, а иногда и желаниях, как все должно было бы быть. У всего этого нет подтвержденной ценности. В 1970-х и 1980-х годах существовали методологии, опирающиеся на прослеживаемость (tracebility), и стремящиеся сократить системные требования вплоть до уровня юнитов. В общем, это NP-трудная (нелинейная полиномиальная) задача (если только вы не выполняете чисто процедурную декомпозицию), поэтому я очень скептичен в отношении всех, кто говорит, что способен её решить. В итоге, единственный вопрос, который следовало бы задавать каждому тесту: Если тест упадет, какое из бизнес-требований будет нарушено? В большинстве случаев, ответ: Я не знаю. Если вы не понимаете ценность теста, тогда, теоретически, он может иметь нулевую ценность для бизнеса. У теста есть стоимость: поддержка, время вычислений, администрирование и так далее. Значит, у теста может быть чистая отрицательная ценность. И это четвертая категория тестов, которые необходимо удалять. Такие тесты, не смотря на их способность что-то проверять, в действительности ничего не проверяют.

Если вы не можете объяснить, как сбой какого-либо юнит теста отражается на рисках, следует подумать о возможном удалении такого теста. При отсутствии формальных критериев корректности, существуют более эффективные техники устранения изъянов в качестве, такие как исследовательское тестирование и методы Монте-Карло. (Они великолепны, и я рассматриваю их как отдельную категорию в отличие от того, о чем рассказываю здесь.) Не используйте юнит тесты для подобных проверок.

Обратите внимание, существуют определенные модули, а также тесты, которые дают четкий ответ на вопрос о ценности для бизнеса. Одни из таковых - это регрессионные тесты; они, однако, редко пишутся на уровне модульных тестов, больше - на системном. При падении регрессионного теста, в силу его внутреннего устройства, мы точно знаем об особенности ошибки. Кроме того, в определенных системах существуют ключевые алгоритмы, типа алгоритмов сетевой маршрутизации, которые можно протестировать за счет API. Как я уже отмечал выше, для таких API существует формальный оракул, на основании которого можно создавать тесты. Такие юнит тесты имеют ценность.

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

1.6 Сложное - сложно

Существует следующая дилемма: большая часть интересных показателей о качестве определенных программ находиться в распределении результатов тестирования, несмотря на то, что традиционные подходы к статистике, всё же, предоставляют ложную информацию. Так, в 99,99% всех случаев тест может быть успешным, но однажды упав за десять тысяч раз, он убьет вас. Опять же, заимствуя аналогию из мира железа, для уменьшения вероятности ошибки до сколь угодно низкого уровня, вы можете всё проектировать с учетом заданной вероятности отказа или же провести анализ наихудшего случая (WCA). Специалисты по аппаратному обеспечению обычно используют WCA при проектировании асинхронных систем для защиты от сбоев в сигналах, выходящих за пределы проектных параметров: один сбой на 100 миллионов раз. В области аппаратного обеспечения, сказали бы, что коэффициент качества (FIT rate) такого модуля равняется 10 - десять отказов на триллион (Failures In a Trillion).

Клиент, упомянутый мной в начале этой статьи, был озадачен причиной отсутствия положительных результатов тестирования в своей текущей команде, по сравнению с предыдущей работой. Я отправил ему более раннюю версию данной статьи, на что он ответил:

Приятно читать статью, проливающую свет на причину моего успеха (и остальной части моей команды). Возможно, Вы уже знаете, что я инженер по авионике, чья карьера началась с разработки встраиваемого программного обеспечения, и, отчасти, разработки оборудования. И вот с таким образом мышления, ориентированным на особенности работы оборудования, я начал тестировать свое программное обеспечение. (Команда состояла из четырех человек: 3-х инженеров-электриков из Делфтского университета (включая меня, в качестве специалиста по авионике) и одного инженера-программиста (из Гаагского университета). Мы были очень дисциплинированы в разработке систем безопасности для банков, пенитенциарных учреждений, пожарных, полицейских участков, служб экстренной помощи, химических заводов и т.д. В каждом из случаев всё должно было правильно заработать с первого раза.)

Обычно, при наличии разумных предположений, вы можете провести WCA аппаратного обеспечения, из-за легко прослеживаемых причинно-следственных связей: можно взять схему и разобраться в причинах изменения состояний в элементах памяти. Состояния в машине фон Неймана изменяются в результате побочных эффектов (side effects) выполнения функции, и, как правило, отследить причину такого изменения нереально, даже если некое состояние является достижимым. Объектная ориентация еще больше усугубляет ситуацию. Невозможно понять, какая из инструкций последней изменила состояние, используемое программой.

Большинство программистов убеждены, что построчное покрытие исходного кода, или, по крайней мере, покрытие ветвлений является вполне достаточным. Нет. С точки зрения теории вычислений, покрытие наихудшего случая означает анализ всевозможных комбинаций в последовательностях работы машинного языка, при котором гарантируется достижение каждой инструкции, а также - воспроизведение каждой возможной конфигурации битов данных в каждом из значений счетчика команд выполняемой программы. (Недостаточна и симуляция состояния среды выполнения только лишь для модуля или класса, содержащего тестируемую функцию или метод: как правило, любое изменение в каком-либо месте может проявиться в любом другом месте программы, а поэтому, потребует повторного тестирования всей программы. Формальное доказательство предложено в статье: Перри и Кайзера (Perry and Kaiser), Адекватное тестирование и объектно-ориентированное программирование (Adequate Testing and Objectoriented Programming), Журнал объектно-ориентированного программирования 2 (5), январь 1990 г., стр. 13). Даже взяв небольшую программу, мы уже попадаем в такое тестовое окружение, количество комбинаций в котором намного превышает количество молекул во Вселенной. (Мое определение понятия покрытие кода - это процент всех возможных пар, {Счетчик команд, Состояние системы}, воспроизводимых вашим набором тестов; все остальное - эвристика, которую, очевидно, вам сложно будет как-либо обосновать). Большинство выпускников бакалавриата смогут распознать проблему остановки (Halting Problem) в большинстве вариантов подобных задачах и поймут, что это невозможно.

1.7 Меньше - это больше или вы не шизофреник

Вот еще одна проблема, которая имеет особое отношение к первоначальному вопросу моего клиента. Наивный тестировщик пытается извлечь множество данных из результатов тестирования, при этом постоянно поддерживая все существующие тесты или даже добавляя новые; это приводит к точно такой же ситуации, в которой оказался мой клиент, когда сложность тестов (объемы кода или какие-только-хотите-метрики) начинает превосходить сложность исходного кода. Тестируемые классы - это код. Тесты - это код. Разработчики пишут код. Когда разработчики пишут код, они допускают около трех ошибок, непосредственно влияющих на систему, на каждые тысячу строк кода. Если бы мы случайным образом выбрали участки кода с подобными ошибками у моего клиента, включая тесты, то обнаружили бы, что в тестах содержиться код, который приводит к неправильным результатам чаще, чем реальный баг, останавливающий выполнение кода!

Некоторые мне говорят, что подобное не имеет к ним отношения, поскольку они уделяют значительно больше внимания тестам, чем исходному коду. Во-первых, это просто вздор. (Меня действительно смешат утверждающие, что, с одной стороны, они способны забывать о своих ранее сделанных предположениях во время создания изначального кода, и, с другой, те, кто может привнести свежий и независимый взгляд во время тестирования. Как первые, так и вторые должны быть шизофрениками.) Посмотрите, что делают разработчики при запуске тест-сьютов: они их запускают, но не думают (кстати, это же относится и к большей части Agile манифеста). На моей первой работе в Дании был проект, в значительной степени построенный на XP методологии и юнит тестировании. Я всячески пытался собрать билд на своей локальной машине, и после долгой борьбы с Maven и другими инструментами, наконец-то, мне это удалось. Каким же было разочарование, когда я обнаружил, что юнит-тесты не проходят. Пришлось обратиться к своим коллегам, которые сказали: О, так тебе нужно запустить Maven с вот этим флагом, он отключает вот эти тесты - из-за изменений эти тесты уже не работают, поэтому их необходимо отключить.

Если у вас 200, 2000, или 10 000 тестов, вы не будете тратить время на тщательное исследование и (кхе-кхе) рефакторинг каждого из них каждый раз, когда тест падает. Самая распространенная практика, которую я наблюдал, работая в стартапе еще в 2005 году, - это просто переписать результат старых тестов (ожидаемый результат или результаты вычислений такого теста) новыми результатами. С психологической перспективы, зеленый статус - это вознаграждение. Современные быстрые машины создают иллюзию возможности замены мышления программиста; их скорость намекает на исключение моей необходимости мыслить. Ведь, в любом же случае, если клиент сообщит об ошибке, я, в свою очередь, сформулирую гипотезу о ее действительной причине, внесу изменения, исправляющие поведение системы, и, в результате, с легкостью смогу себя убедить, что функция, в которую я добавил исправление, теперь работает правильно. То есть я просто переписываю результат выполнения этой функции. Однако, подобное - просто лженаука, основанная на колдовстве, связь с которым - причинность. В таком случае, необходимо повторно запустить все регрессионные и системные тесты.

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

1.8 Вы платите за поддержку тестов и качество!

Суть в том, что код - это часть вашей системной архитектуры. Тесты - это модули. Тот факт, что кто-то может не писать тесты, не освобождает его от ответственности заниматься проектированием и техническим обслуживанием возрастающего количества модулей. Одна из методик, которую часто путают с юнит-тестированием, но использующая последнее в качестве техники - это разработка через тестирование (TDD). Считается, что она улучшает метрики сцепления и связности (coupling and coherence), хотя, эмпирические данные свидетельствуют об обратном (одна из статей, опровергающих подобное представление на эмпирических основаниях принадлежит Янзену и Саледиану (Janzen and Saledian), Действительно ли разработка через тестирование улучшает качество проектирования программного обеспечения? IEEE Software 25(2), март/апрель 2008 г., стр. 77 - 84.) Еще хуже то, что таким образом, в качестве запланированного изменения, вы уже вводите связанность (coupling) между каждым модулем и сопровождающими их тестами. У вас появляется необходимость относиться к тестам так же как и к системным модулям. Даже если вы удаляете их перед релизом, это никак не сказывается на необходимости их обслуживать. (Подобное удаление может быть даже достаточно плохой идеей, но об этом дальше.)

Я обратил внимание, что подавляющая часть юнит-тестов, особенно созданных с помощью JUnit, представляет собой замаскированные утверждения (assertions). Когда же я создаю какое-нибудь восхитительное программное обеспечение, то засыпаю его подобными утверждениями, описывая ожидаемые мной результаты, и по отношению к тем, кто вызывает мои функции, и в отношении самих функций, которые обязаны что-то предоставить своим клиентам. Такие проверки дорабатываются в том же артефакте, где находится и остальная часть моего кода. Множество энвайерментов предоставляют средства административной стерилизации этих тестов во время релиза.

Более профессиональный подход - оставить эти асершены в коде даже после релиза, автоматически отправлять отчеты об ошибках от имени конечного пользователя и, возможно, пытаться перезапускать приложение каждый раз, когда подобная проверка провалена. В одном из ранее упомянутых стартапов, мой босс настаивал, чтобы мы такого не делали. Я указал, что отрицательный результат проверок означает: в программе что-то пошло совсем не так, и, скорее всего, данная программа выдаст неправильный результат. Даже мельчайшая ошибка в создаваемом нами продукте, может обойтись клиенту в 5 миллионов долларов дополнительных доработок. На что он ответил: Для компании намного важнее избегать видимости чего-то неправильно сделанного, чем останавливаться еще до получения ошибочных результатов. Я ушел из этой компании. Возможно, сегодня вы один из её клиентов.

Превратите юнит-тесты в утверждения (assertions). Используйте их для обеспечения отказоустойчивости архитектуры высокодоступных систем. Это решает проблему поддержки множества дополнительных программных модулей, которые оценивают ход выполнения (программы) и проверяют правильность (её) поведения; это одна часть юнит-тестирования. Другая - это драйвер, выполняющий сам код: в этом отношении, рассчитывайте, на свои стресс-, интеграционные и системные тесты.

Почти последнее, существуют такие юнит-тесты, которые повторяют системные, интеграционные или другие виды тестов. На заре вычислений, когда компьютеры были медленными, вместо того, чтобы дожидаться запуска системных тестов, юнит-тесты предоставляли разработчику более быструю обратную связь о том, сломало ли их изменение код. Сегодня, когда появились более дешевые и мощные компьютеры, этот аргумент кажется менее убедительным. Каждый раз, внося изменения в свое приложение Scrum Knowsy, я тестирую его на системном уровне. Разработчики должны непрерывно интегрироваться и, так же непрерывно проводить тестирование системы, а не сосредотачиваться на своих юнит-тестах и откладывать интеграцию, даже на час. Так что избавляйтесь от юнит-тестов, которые дублируют то, что уже делают системные тесты. Если системный уровень обходится слишком дорого, создайте наборы интеграционных тестов. Рекс (Rex) считает, что следующим большим скачком в тестировании будет разработка таких юнит, интеграционных и системных тестов, которые устраняют случайные упущения и дублирование.

Проверьте свои тестовые наборы на предмет репликации; вы же способны профинансировать это в рамках своих гибких (Lean) программ. В первую очередь, создавайте системные тесты с хорошим функциональным покрытием (а не покрытием кода) - помните, что правильная реакция на неправильные входные данные или другие непредвиденные условия является частью ваших функций.

Последнее: однажды я от кого-то услышал оправдание в необходимости создания (особого) юнит-теста, из-за невозможности проверить некий блок кода с помощью внешнего интерфейса инструмента для тестирования. Если учесть, что подобные интерфейсы хорошо спроектированы и способны воспроизводить различное, наблюдаемое в реальном мире, поведение программы, и вы нашли такой код, к которому не достучаться из вашей тестировочной системы, тогда... удалите этот код! Серьезно, размышление о коде с позиции системных тестов может быть отличным способом для обнаружения мертвого кода. Это даже более ценно, чем поиск ненужных тестов.

1.9 Это процесс, глупец или лихорадка зеленого статуса

Возможно, самая серьезная проблема юнит-тестов заключается в том, что они нацелены на исправление ошибок, а не улучшение системы. Я довольно-таки часто встречал программистов, которые, склонив головы, пытались добиться успешного прохождения и, тем самым, повысить зеленые показатели. Тестировщик формулирует гипотезу, но, находясь в своей изолированной среде, он не может с легкостью получить достаточно информации, чтобы ее можно было подтвердить или опровергнуть. Поэтому он или она просто пытаются сделать хотя бы что-то, дабы понять, приближает ли это вас к зеленому статусу - и насколько это вообще возможно.

Тестирование преследует две потенциальные цели. Одна из них - обучение: для большего ознакомления с программой и тем, как она работает. Вторая - использовать его как оракул. Обычно, люди сталкиваются с ошибками во время обращения именно к этой цели: тест - это оракул, а цель - правильный результат. Они упускают из виду тот факт, что цель определяет обширное понимание, а понимание дает ключ к исправлению ошибки.

Именно поэтому необходимо ненадолго отрываться от терминала. Так вы уходите от рефлекса Павлова завязанного на появлении зеленого статуса и, основываясь на тестах, можете начать выстраивать свое общее понимание шаг за шагом. Когда его будет достаточно, у вас появится костяк общей картины. Если и он будет достаточно полным, ошибка станет очевидной.

Системные тесты почти сразу же погружают вас в подобную позицию размышления. Разумеется, вам все еще нужна будет более подробная информация, для этого на помощь приходит отладка (debugging). Отладка - это использование инструментов и устройств, помогающих изолировать ошибку. Отладка - не тестирование. Во-первых, она представляет собой ad-hoc (интуитивную активность) и, во-вторых, выполняется на основании последовательного перехода от ошибки к ошибке. Юнит тесты могут быть полезным инструментом отладки. На собственном опыте я обнаружил, что лучше всего работает комбинация различных инструментов, среди которых наиболее эффективными являются наборы с невалидными данными и доступ к глобальному контексту, включающий все значения данных и случайную трассировку стека.

1.10 Подводим итоги

Вернемся к моему клиенту из компании Sogeti. Вначале, я упоминал его высказывание:

Когда я ежедневно программировал, то создавал действительно тестируемый код, однако почти никогда не писал никакие юнит тесты. При этом я заслуживал признание за свой качественный код и почти безошибочное программное обеспечение. Мне хотелось бы разобраться, ПОЧЕМУ такой вариант работал в моем случае?

Возможно, Ричард - это один из тех редких людей, которые знают, как думать самому, вместо того, чтобы позволять компьютеру думать за него - будь то проектирование системы или дизайн более низкого уровня. Я чаще наблюдаю подобную ситуацию в Восточноевропейских странах, где отсутствие общедоступного компьютерного оборудования заставляло людей мыслить. Просто не хватало компьютеров. Когда я впервые посетил Сербию в 2004 году, студенты из ФОН (факультета информатики) могли получить доступ к компьютеру для выхода в интернет один раз в неделю. Расплата за ошибку высока: если запуск вашего кода не отработает, придется ждать еще неделю, чтобы повторить свою попытку.

К счастью, я вырос именно в такой культуре программирования, мой код записывался на перфокартах, которые отдавались оператору для установки в очередь машины, а затем, через сутки, собирались результаты. Такой формат действительно заставлял вас или же задуматься - или же, потерпеть неудачу. У Ричарда из Sogeti было аналогичное воспитание: у них была неделя на подготовку кода и всего один час на его запуск. Всё должно было делаться правильно с первого раза. В любом случае, обдуманный проект должен оценивать возможные риски, связанные с затратами, и устранять их по одному в каждой итерации, уделяя особое внимание постоянно растущей ценности. Одна из моих любимых циничных цитат: Я считаю, что недели программирования и тестирования могут сэкономить мне часы планирования. Что меня больше всего беспокоит в культуре раннего провала (fail-fast), так это не столько понятие провала, сколько слово раннее. Много лет назад мой босс Нил Халлер мне сказал, что отладка - это не то, что вы делаете, сидя перед своей программой с отладчиком; это то, что вы делаете, откинувшись на спинку стула и глядя в потолок, или обсуждение ошибки с командой. Однако многие, якобы ярые приверженцы эджайл методологий, ставят процессы и JUnit выше людей и взаимодействий.

Лучший пример, услышанный мной в прошлом году, был от моей коллеги, Нэнси Гитинджи (Nancy Githinji), управлявшей вместе со своим мужем IT-компанией в Кении; сейчас они оба работают в Microsoft. Последний раз, посещая свой дом (в прошлом году), она познакомилась с детьми, которые проживают в джунглях и пишут программы. Они могут приезжать раз в месяц в город, чтобы получить доступ к компьютеру и апробировать свой код. Я хочу нанять этих детей!

Мне, как стороннику эджайла (да и просто из принципа), немного больно признавать, что Рекс оказался прав, как, впрочем-то это было и ранее , достаточно красноречиво сказав: В этой культуре раннего провала (fail fast) есть нечто небрежное, она побуждает швырнуть кучу спагетти на стену, особо даже не задумываясь отчасти, из-за чрезмерной уверенности в заниженных рисках, предоставляемых юнит-тестами. Культура раннего провала может хорошо работать при очень высокой дисциплине, подкрепленной здоровым скептицизмом, однако редко можно встретить такое отношение в динамичном IT-бизнесе. Иногда ошибки требуют обдумывания, а последнее требует больше времени, чем результаты, достигаемые ранним провалом. Как только что напомнила моя жена Гертруда: Никто не хочет, чтобы ошибки затягивались на долго

Если вы наймете профессионального менеджера или консультанта по тестированию, он сможет помочь вам разобраться в вопросах более масштабной картины тестирования: интеграционным и системным тестированием, а также подходящим для них инструментарием и процессами. Это важно. Но не забывайте о позиции продакт овнера, бизнес-аналитика или менеджера в Scrum: управление рисками находится в центре их деятельности, и, возможно, именно поэтому Джефф Сазерленд говорит, что заказчик должен разрабатывать (а в лучшем случае и проектировать) системные тесты для использования в качестве входных данных до или во время планирования спринта.

Что касается Интернета: грустно и откровенно страшно, что там так много всего. Много советов, но очень мало из них подкреплено теорией, данными или даже моделью того, почему необходимо довериться тому или иному совету. Хорошее тестирование немыслимо без скептицизма. Относитесь скептически к себе: измеряйте, доказывайте, делайте повторные попытки. Ради всего святого, отнесись ко мне скептически.

Пишите мне свои комментарии на jcoplien@gmail.com с копией Рексу вначале этого письма.

В заключение:

  • Сохраняйте регрессионные тесты до года, большинство из них должны быть тестами системного уровня, а не юнит-тестами.

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

  • Исключая предыдущее заявление, если у X есть определенная бизнес-ценность и вы можете протестировать X системным или же юнит-тестом, используйте системный: контекст - это всё.

  • Разрабатывайте тест более тщательно, чем код.

  • Превратите большинство юнит-тестов в утверждения (assertions).

  • Удалите тесты, которые за год ни разу не падали.

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

  • Если вы обнаруживаете, что определенные тестируемые функции тривиальны, еще раз перепроверьте способы стимуляции производительности разработчиков. Одобрение за покрытие или другие бессмысленные метрики могут привести к быстрому разрушению архитектуры.

  • Будьте скромны в отношении способностей тестов. Тесты не улучшают качество: это делают разработчики.

Подробнее..

Приглашаем на QA Meeting Point

09.10.2020 16:18:18 | Автор: admin
image

20 октября 2020 года DINS проведет онлайн-конференцию для QA-инженеров и разработчиков. Мы хотим объединить инженеров из разных городов России, чтобы вместе обсудить боли, интересные кейсы, проблемы, любимые (и не очень) технологии. Ведущим конференции и модератором круглого стола станет Артем Ерошенко.

Участие бесплатное, но нужно зарегистрироваться.

Под катом программа, спикеры и другие подробности о конференции.


Программа



Топ-10 вредных советов и к чему они могут привести (Ирина Ушакова, EPAM, Нижний Новгород)


На примерах рассмотрим, как на хороших конференциях по тестированию могут звучать спорные советы. В ходе выступления Ирина поделится, как делать точно не надо, и почему.

Ирина Ушакова Senior QA Engineer в EPAM. В IT-сфере более 8 лет. Начинала в качестве аналитика и переквалифицировалась в QA-инженера. Выступала на QA Z-Days 2020.

Автоматизация тестирования мобильного приложения Яндекс.Деньги (Александр Наташкин, Яндекс.Деньги, Санкт-Петербург)


Алекандр поделится кейсом Яндекс.Денег по автоматизации тестирования мобильного приложения. Он расскажет, какие подходы и инструменты сработали, а какие не очень, и как менялись технические потребности с ростом команды.
Поговорим об автотестах, разработке фреймворков и настройке CI, увидим метрики до/после внедрения автоматизации.

Александр Наташкин руководитель мобильного тестирования, Яндекс.Деньги. За 7 лет в QA прошел путь от ручного тестировщика до руководителя. Преподает в учебном центре Otus.

Отлавливаем баги в коде и рабочем процессе (Елена Гурова, Usetech, Ростов-на-Дону)


На реальных примерах посмотрим и разберем баги прямиком из нашего багтрекера, а также рабочих чатиков и совещаний.

Звездами шоу будут:
  • А давайте релиз в пятницу, во второй половине дня?
  • Как это попало в продакшн?!
  • Вы же уже исправляли это, почему опять сломано?

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

Елена Гурова Team Lead QA в Usetech. Елена в тестирование попала случайно, фактически, убегая от саппорта, но оказалось, что эта область самая родная и любимая. Преподает в корпоративном университете компании, читает лекции в GeekBrains и выступает на профильных мероприятиях (#RndTech).

Качество релизов ответственность команды (Людмила Малеева, Miro, Пермь)


Людмила расскажет про процессы тестирования и релизов в Miro: как они взаимосвязаны и как в компании повышают качество релизов. Вы узнаете, с помощью каких инструментов построить процессы, чтобы релизить с большим количеством команд и монорепозиторием.

Людмила Малеева QA engineer в Miro. Работала с автотестами, ручным и нагрузочным тестированием, построением процессов и релизами. Последний год занимает роль Release Manager. Выступала на локальных митапах Xsolla и СКБ Контур.

Круглый стол с программным комитетом QA Meeting Point


Инженеры из DINS обсудят одну из этих тем:

  • Правда или миф: хороший QA-инженер не будет оставаться в профессии, а станет разработчиком
  • Не надо так: деплоймент на продакшн по пятницам и тестирование на прод
  • Стоит ли ломать продакшн для проверки высокой доступности и безопасности


Прими участие в выборе темы в Telegram-канале конференции с 9 октября до 14 октября.

Регистрация


Участие в конференции бесплатное, но нужна предварительная регистрация, чтобы в день мероприятия мы прислали ссылку на онлайн-трансляцию.

Всу участники первыми получат записи докладов с QA Meeting Point.
Подробнее..

QA Online Meetup 2810

23.10.2020 16:09:30 | Автор: admin
Приглашаем на открытый QA meetup, он пройдет 28 октября. Поговорим об инструменте Cypress и посмотрим его в действии, а также обсудим роль оркестраторов бизнес-логики в корпоративных приложениях.

Подключайтесь к нам!



О чем будем говорить


Как мы начали использовать Cypress в тестировании и не прогадали

Марина Парусникова, Райффайзенбанк

О спикере: старший тестировщик в Райффайзенбанке. Имею опыт в тестировании более 6 лет. В начале карьеры занималась ручным тестированием десктопного приложения, написанием юнит-тестов, разработкой эмулятора. В Райффайзенбанке погрузилась в мир автоматизации тестирования веб-приложений, испробовала BDD и TDD на практике, занималась нагрузочным тестированием. Люблю выстраивать удобные процессы в команде, считаю особенно ценным достижение синергии в работе тестировщиков и разработчиков.

О докладе: Cypress набирающий известность фреймворк для автоматизации тестирования веб-приложений. Можно ли им полностью заменить автоматизацию на Selenium+Cucumber? Как начать тестировщикам использовать Cypress для автоматизации тестов? Поделюсь опытом в выступлении.


Оркестр на изоляции: автоматизируем тесты Camunda

Денис Кудряшов, Леруа Мерлен

О спикере: Прошёл стандартный карьерный путь (из тестирования в разработку) наоборот. Ушел из разработки в тестирование более 3-х лет назад, сейчас занимаюсь автоматизацией. Как-то раз тестировал сайт и случайно нашел уязвимость в Яндексе.

О докладе: В докладе речь пойдёт о роли оркестраторов бизнес-логики в корпоративных приложениях. Я расскажу о том, какие системы и каким образом используются в Леруа Мерлен. Мы исследуем систему управления бизнес-процессами Camunda с точки зрения тестирования реализаций BPMN схем, а также поговорим об инфраструктуре тестовых окружений (как мы понимаем и поднимаем их у себя в Леруа). И всё это с примерами!

>> Начнём митап в 18:00 (МСК).
Регистрируйтесь, чтобы получить ссылку на трансляцию: письмо со ссылкой придет вам на почту. Мы вас ждем, до встречи online!
Подробнее..

QA Meeting Point доклады

14.12.2020 16:17:31 | Автор: admin
Привет, Хабр!

В этом году мы в DINS провели нашу первую конференцию для QA-инженеров. К нам подключились более 900 человек из 90 городов России и Европы, чтобы послушать доклады с кейсами и дискуссию круглого стола о карьерных перспективах в тестировании.

Мы рады, что смогли объединить столько классных людей в одной точке, несмотря на удаленку и карантин. А еще больше радуемся, что доклады получились полезными, а дискуссия жаркой, поэтому мы, конечно, хотим ими поделиться. Под катом собрали короткие описания докладов и видео как раз самое оно для долгих зимних вечеров :).

Что делать, если у вас слишком много автотестов


Спикер: Сергей Потанин QAA Team Lead в Wrike, Воронеж.

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


Где логика?! История тестирования одного микросервиса


Спикер: Денис Кудряшов инженер по тестированию в Leroy Merlin, Москва.

Чем сложнее системы в микросервисах, тем больше в них бизнес-логики. Неважно, остается она в микросервисе или выносится в отдельное приложение ее нужно тестировать. Денис рассказал, как методики структурного тестирования помогут справиться с этой задачей. Из кейса Leroy Merlin вы узнаете, как повысить надежность и скорость выполнения тестов с помощью моков и других инструментов.


Автоматизация тестирования мобильного приложения Яндекс.Деньги


Спикер: Александр Наташкин руководитель мобильного тестирования, Яндекс.Деньги, Санкт-Петербург.

Александр поделился кейсом Яндекс.Денег по автоматизации тестирования мобильного приложения. Он рассказал, какие подходы и инструменты сработали, а какие не очень, и как менялись технические потребности с ростом команды. Поговорили об автотестах, разработке фреймворков и настройке CI, рассмотрели метрики до/после внедрения автоматизации.


Отлавливаем баги в коде и рабочем процессе


Спикер: Елена Гурова Team Lead QA в Usetech, Ростов-на-Дону.

Елена разобрала реальные баги прямиком из багтрекера Usetech, рабочих чатиков и совещаний.

Звездами шоу стали:
  • А давайте релиз в пятницу, во второй половине дня?
  • Как это попало в продакшн?!
  • Вы же уже исправляли это, почему опять сломано?

И другие жизненные истории и способы их решения.


Качество релизов ответственность команды


Спикер: Людмила Малеева QA engineer в Miro, Пермь.

Людмила рассказала про процессы тестирования и релизов в Miro: как они взаимосвязаны и как в компании повышают качество релизов. Вы узнаете, с помощью каких инструментов построить процессы, чтобы релизить с большим количеством команд и монорепозиторием.


Правда или миф: хороший QA-инженер не будет оставаться в профессии, а станет разработчиком


Инженеры DINS встретились за виртуальным круглым столом и поговорили о реальных перспективах развития в качестве QA-инженера. Стоит ли QA-инженеру становиться разработчиком? Сколько можно заработать в этой профессии? Просто ли достичь вершины карьеры или придется сильно постараться, чтобы выделиться? Смотрите и делитесь своими мнениями в комментариях!


QA Meeting Point 2020: как это было


Кроме докладов мы позаботились и о подарках и развлечении участников конференции: провели флешмоб и зарядку, раздали мерч за лучшие вопросы, разыграли участие в спортивном марафоне от LiveBody и Apple Watch 6. Немного подробнее в видео ниже.

Подробнее..

Из песочницы Тесты на pytest с генерацией отчетов в Allure с использованием Docker и Gitlab Pages и частично selenium

02.08.2020 00:20:53 | Автор: admin

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


Пример отчета, получающийся в allure


Когда я хотел добавить в gitlab автотесты в стеке python, allure, docker, то я выяснил, что толковых статей на эту тему нет. Пришлось разбираться самостоятельно и как результат проб и ошибок появилась эта статья, которая скорее является гайдом, частично затрагивающим написание тестов, но наибольший фокус именно на выстраивании инфраструктуры. Если у вас уже написаны тесты на allure, то вы сразу можете переходить к разделу настройки инфраструктуры. Отмечу, что текст НЕ затрагивает написание UI тестов, но я затрону инфраструктуру для них в отдельном блоке.


Здесь не описаны лучшие практики по написанию тестов и выстраиванию инфраструктуры. Скорее ознакомление с тем, как можно все это организовать. Примеры выполнены на windows, на других системах подход примерно такой же.


Этот гайд предполагает, что у вас уже установлен python, настроен пустой репозиторий, куда вы будете отправлять свой код. При чтении статьи не обязательно знать pytest, но очень желательно знать что из себя представляет фикстура, как ей пользоваться, а так же параметризация тестов. Для тестирования будет использован Dog API. Если вас интересует локальный запуск тестов через gitlab, то вам необходимо установить docker.


Если у вас не установлено вышеперечисленное ПО, то ниже вы можете найти полезные ссылки:



Содержание


  • Подготовка к написанию тестов
  • Написание фикстуры и небольшого клиента для API тестов
  • Пишем тесты
  • Добавляем allure в тесты
  • Делаем инфраструктуру для тестов
  • Настройка репозитория
  • Установка и настройка Gitlab Runner
  • Настройка пайплайна в .gitlab-ci.yml
  • Запуск тестов и просмотр отчета
  • Настройка пайплайна для UI тестов
  • Полезные ссылки

Подготовка к написанию тестов


Я назвал свой репозиторий для этой статьи allure_pages и скопировал его на свой компьютер.


Нужно создать такую структуру в директории с репозиторием:



  • conftest.py будет использован для написания фикстур
  • requirements.txt для установки необходимых модулей в контейнере с python
  • test_dog_api.py внутри отдельной директории tests для написания самих тестов

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


python -m venv venv


Открываем папку с репозиторием как проект в PyCharm



Удобнее всего, на мой взгляд устанавливать нужные нам пакеты через встроенный в PyCharm терминал. Для этого нужно добавить интерпретатор, и открыть терминал.


  1. Жмем на Add Interpreter и если вы ранее создали в директории с проектом виртуальное окружение, то PyCharm сразу найдет нужный интерпретатор, который будет использован в проекте
  2. Открываем терминал по этой кнопке


Устанавливаем нужные пакеты для наших тестов в виртуальном окружении:


Для этого введите в терминал следующие команды.


  • pip install requests для запросов, которые мы будем делать к сервису Dog Api
  • pip install pytest для тестов, само собой
  • pip install pytest-xdist для параллелизации тестов
  • pip install allure-pytest для генерации отчетов в allure

Очень важно после этого написать в терминале
pip freeze > requirements.txt.


Файл requirements.txt нам понадобится для запуска тестов внутри докер контейнера.

Написание фикстуры и небольшого клиента для API тестов


Для того, чтобы упростить наши тесты создадим небольшой клиент для GET и POST запросов. Далее будет подробное описание этого клиента. Если вам это не нужно или кажется сложным, то пропустите эту часть статьи и переходите к следующей. На примере её использования станет ясно как пользоваться клиентом.


Сама фикстура


@pytest.fixturedef dog_api():    return ApiClient(base_address="http://personeltest.ru/aways/dog.ceo/api/")

Ниже конструктор класса для клиента. При инициализации объекта класс принимает в себя аргумент base_address, на его место нужно записать в фикстуре общий адрес для всех запросов в нашем случае будет использован https://dog.ceo/api/. Обратите внимание, что фикстура возвращает объект с базовым адресом для дальнейших GET и POST запросов.


class ApiClient:    def __init__(self, base_address):        self.base_address = base_address

Добавим в класс метод GET для отправки запросов.


    def get(self, path="/", params=None, headers=None):        url = f"{self.base_address}{path}"        return requests.get(url=url, params=params, headers=headers)

Здесь можно заметить, что наш клиент является оберткой над библиотекой requests. В классе ApiClient аргументы path, params, headers метода get выступают в роли аргументов, которые передаются в requests.get(url=url, params=params, headers=headers). Именно эта строчка и отвечает за выполнение запросов. Из адреса, использованного при инициализации объекта и пути, который мы передадим в дальнейшем в ходе теста, будет складываться полный адрес, который и будет использоваться в requests.get.


Добавим в класс метод POST. Его логика идентична, просто добавляется несколько аргументов, которые при желании можно заполнить данными.


    def post(self, path="/", params=None, data=None, json=None, headers=None):        url = f"{self.base_address}{path}"        return requests.post(url=url, params=params, data=data, json=json, headers=headers)

В итоге у нас получается такой файл conftest.py:


import pytestimport requestsclass ApiClient:    def __init__(self, base_address):        self.base_address = base_address    def post(self, path="/", params=None, data=None, json=None, headers=None):        url = f"{self.base_address}{path}"        return requests.post(url=url, params=params, data=data, json=json, headers=headers)    def get(self, path="/", params=None, headers=None):        url = f"{self.base_address}{path}"        return requests.get(url=url, params=params, headers=headers)@pytest.fixturedef dog_api():    return ApiClient(base_address="http://personeltest.ru/aways/dog.ceo/api/")

Пишем тесты


В самом сервисе Dog API есть много разных запросов. Если у вас есть опыт в написании тестов, то можете самостоятельно написать несколько тестов. Так как акцент в этой статье заключается в постройке инфраструктуры для тестов, я не буду вдаваться в подробности тестов. Ранее я это сделал, чтобы мой код был более менее понятен. Несколько параметризованных тестов специально сломаны, чтобы показать как они будут показаны в отчете. Так выглядит мой файл с тестами test_dog_api.py


import pytestdef test_get_random_dog(dog_api):    response = dog_api.get("breeds/image/random")    with allure.step("Запрос отправлен, посмотрим код ответа"):        assert response.status_code == 200, f"Неверный код ответа, получен {response.status_code}"    with allure.step("Запрос отправлен. Десериализируем ответ из json в словарь."):        response = response.json()        assert response["status"] == "success"    with allure.step(f"Посмотрим что получили {response}"):        with allure.step(f"Вложим шаги друг в друга по приколу"):            with allure.step(f"Наверняка получится что-то интересное"):                pass@pytest.mark.parametrize("breed", [    "afghan",    "basset",    "blood",    "english",    "ibizan",    "plott",    "walker"])def test_get_random_breed_image(dog_api, breed):    response = dog_api.get(f"breed/hound/{breed}/images/random")    response = response.json()    assert breed in response["message"], f"Нет ссылки на изображение с указанной породой, ответ {response}"@pytest.mark.parametrize("file", ['.md', '.MD', '.exe', '.txt'])def test_get_breed_images(dog_api, file):    response = dog_api.get("breed/hound/images")    response = response.json()    result = '\n'.join(response["message"])    assert file not in result, f"В сообщении есть файл с расширением {file}"@pytest.mark.parametrize("breed", [    "african",    "boxer",    "entlebucher",    "elkhound",    "shiba",    "whippet",    "spaniel",    "dvornyaga"])def test_get_random_breed_images(dog_api, breed):    response = dog_api.get(f"breed/{breed}/images/")    response = response.json()    assert response["status"] == "success", f"Не удалось получить список изображений породы {breed}"@pytest.mark.parametrize("number_of_images", [i for i in range(1, 10)])def test_get_few_sub_breed_random_images(dog_api, number_of_images):    response = dog_api.get(f"breed/hound/afghan/images/random/{number_of_images}")    response = response.json()    final_len = len(response["message"])    assert final_len == number_of_images, f"Количество фото не {number_of_images}, а {final_len}"

Добавляем allure в тесты


Есть много разных подходов к написанию тестов с помощью allure. Так как сейчас рассматривается больше инфраструктура, то добавим совсем простые шаги. Для понимания написанного мною кода, я все же прокомментирую несколько моментов.


with allure.step('step 1'): с помощью этого контекстного менеджера тело теста делится на шаги, понятные в отчете.
@allure.feature('Dog Api') @allure.story('Send few requests') декораторы, с помощью которых сами тесты или тестовые наборы будут структурированы в отчете


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

Добавим в наш API клиент несколько allure шагов.


class ApiClient:    def __init__(self, base_address):        self.base_address = base_address    def post(self, path="/", params=None, data=None, json=None, headers=None):        url = f"{self.base_address}{path}"        with allure.step(f'POST request to: {url}'):            return requests.post(url=url, params=params, data=data, json=json, headers=headers)    def get(self, path="/", params=None, headers=None):        url = f"{self.base_address}{path}"        with allure.step(f'GET request to: {url}'):            return requests.get(url=url, params=params, headers=headers)

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


@allure.feature('Random dog')@allure.story('Получение фото случайной собаки и вложенные друг в друга шаги')def test_get_random_dog(dog_api):    response = dog_api.get("breeds/image/random")    with allure.step("Запрос отправлен, посмотрим код ответа"):        assert response.status_code == 200, f"Неверный код ответа, получен {response.status_code}"    with allure.step("Запрос отправлен. Десериализируем ответ из json в словарь."):        response = response.json()        assert response["status"] == "success"    with allure.step(f"Посмотрим что получили {response}"):        with allure.step(f"Вложим шаги друг в друга по приколу"):            with allure.step(f"Наверняка получится что-то интересное"):                pass

Сходным образом заполним шагами и декораторами остальные тесты. Финальный файл test_dog_api.py


import pytestimport allure@allure.feature('Random dog')@allure.story('Получение фото случайной собаки и вложенные друг в друга шаги')def test_get_random_dog(dog_api):    response = dog_api.get("breeds/image/random")    with allure.step("Запрос отправлен, посмотрим код ответа"):        assert response.status_code == 200, f"Неверный код ответа, получен {response.status_code}"    with allure.step("Запрос отправлен. Десериализируем ответ из json в словарь."):        response = response.json()        assert response["status"] == "success"    with allure.step(f"Посмотрим что получили {response}"):        with allure.step(f"Вложим шаги друг в друга по приколу"):            with allure.step(f"Наверняка получится что-то интересное"):                pass@allure.feature('Random dog')@allure.story('Фото случайной собаки из определенной породы')@pytest.mark.parametrize("breed", [    "afghan",    "basset",    "blood",    "english",    "ibizan",    "plott",    "walker"])def test_get_random_breed_image(dog_api, breed):    response = dog_api.get(f"breed/hound/{breed}/images/random")    with allure.step("Запрос отправлен. Десериализируем ответ из json в словарь."):        response = response.json()    assert breed in response["message"], f"Нет ссылки на фото с указанной породой, ответ {response}"@allure.feature('List of dog images')@allure.story('Список всех фото собак списком содержит только изображения')@pytest.mark.parametrize("file", ['.md', '.MD', '.exe', '.txt'])def test_get_breed_images(dog_api, file):    response = dog_api.get("breed/hound/images")    with allure.step("Запрос отправлен. Десериализируем ответ из json в словарь."):        response = response.json()    with allure.step("Соединим все ссылки в ответе из списка в строку"):        result = '\n'.join(response["message"])    assert file not in result, f"В сообщении есть файл с расширением {file}"@allure.feature('List of dog images')@allure.story('Список фото определенных пород')@pytest.mark.parametrize("breed", [    "african",    "boxer",    "entlebucher",    "elkhound",    "shiba",    "whippet",    "spaniel",    "dvornyaga"])def test_get_random_breed_images(dog_api, breed):    response = dog_api.get(f"breed/{breed}/images/")    with allure.step("Запрос отправлен. Десериализируем ответ из json в словарь."):        response = response.json()    assert response["status"] == "success", f"Не удалось получить список изображений породы {breed}"@allure.feature('List of dog images')@allure.story('Список определенного количества случайных фото')@pytest.mark.parametrize("number_of_images", [i for i in range(1, 10)])def test_get_few_sub_breed_random_images(dog_api, number_of_images):    response = dog_api.get(f"breed/hound/afghan/images/random/{number_of_images}")    with allure.step("Запрос отправлен. Десериализируем ответ из json в словарь."):        response = response.json()    with allure.step("Посмотрим длину списка со ссылками на фото"):        final_len = len(response["message"])    assert final_len == number_of_images, f"Количество фото не {number_of_images}, а {final_len}"

Делаем инфраструктуру для тестов


Здесь есть несколько моментов, о которых стоит написать:


  • .gitlab-ci.yml файл, где на языке разметки yaml описаны инструкции что нужно делать gitlab runner
  • gitlab-runner это проект, написанный на языке Go. Он будет выполнять инструкции. Есть несколько вариантов его использования. Мы будем писать инструкцию для gitlab runner, где он в свою очередь будет использовать docker для запуска тестов и всего остального. Далее по тексту он будет обозначаться просто "раннер". Что это за сущность и с чем её едят можно найти здесь.

Если у вас в компании уже настроена инфраструктура и вы можете попросить своего devops выделить вам раннер где-то в облаке, то сделайте это. Раннер можно использовать и локально на своем компьютере. В этом случае вам нужен установленный docker desktop на windows. В этом гайде мы будем использовать локальный раннер, но для облачного запуска вам нужно будет просто указать в .gitlab-ci нужный раннер.


Если у вас нет docker desktop, то здесь инструкция как его заиметь.

Настройка репозитория


Нужно зайти в настройки репозитория Settings -> General -> Visibility, project features, permissions, активировать Pipelines и сохранить изменения.



После этого в разделе настроек появится раздел CI / CD. Переходим Settings -> CI / CD -> Runners



Здесь переходим по ссылке в пункте 1 и согласно инструкции ставим себе на компьютер Gitlab Runner. Далее в статье адаптация инструкции на русском.


Установка и настройка Gitlab Runner


Ниже инструкция, которую вы можете найти здесь. Можете пройтись по ней самостоятельно. Далее в тексте будет сделано все по инструкции.


  1. Создайте папку где-нибудь в системе, например: C:\GitLab-Runner.
  2. Скачайте бинарник для x86 или amd64 и положите его в созданную папку. Переименуйте скачанный файл в gitlab-runner.exe. Вы можете скачать бинарник для любой доступной версии здесь
  3. Запустите командную строку с правами администратора. (Сам я буду использовать обычный powershell с правами администратора)
  4. Зарегестрируйте раннер
  5. Установите раннер как сервис и запустите его. Вы можете запустить службу, используя встроенную системную учетную запись (рекомендуется) или учетную запись пользователя.

С первыми тремя пунктами инструкции все понятно. Создаем папку, кладем в неё переименованный раннер и запускаем в директории powershell, то с четвертым и пятым пунктом могут возникнуть проблемы и недопонимание, если не потратить некоторое время на чтение документации. Далее простыми словами описаны шаги, которые нужно сделать, чтобы раннер заработал.


Я создал папку C:\gitlab_runners и сохранил туда скачанный для моей системы раннер, предварительно переименовав в gitlab-runner.exe


Как только вы справились с первыми тремя пунктами, то нужно сделать следующее:


  1. Ввести команду:
    ./gitlab-runner.exe register
  2. Ввести url, указанный на странице настроек раннеров в репозитори в пункте 2. Например, у меня так:
    https://gitlab.somesubdomain.com/
  3. Ввести токен указанный на странице настроек раннеров в репозитори в пункте 3. Например, у меня так:
    tJTUaJ7JxfL4yafEyF3k
  4. Вводим описание раннера. Его потом можно будет изменить через UI в настроках раннера в репозитории. Например так:
    Runner on windows for autotests
  5. Добавляем теги для раннера, для того, чтобы описывать нужный раннер в .gitlab-ci.yml, используя определенные теги. Это в основном нужно, когда раннеров больше одного.
    docker, windows
  6. Выбираем тип раннера, который нам нужен. Здесь выберем docker
    docker
  7. Вводим дефолтный image, который будет использоваться раннером. Его можно будет изменить в конфиге раннера или указать конкретый в .gitlab-ci.yml
    python:3.8-alpine

Скрншот с выполнеными шагами



Наш раннер зарегистрирован, теперь его нужно запустить.


Проверить статус раннера можно так:
.\gitlab-runner.exe status


Запустите раннер с помощью:
.\gitlab-runner.exe run


Теперь, если вы зайдете в настроки раннеров в репозитории Settings -> CI / CD -> Runners, то увидите что-то такое:



Это означает, что у репозитория есть линк с раннером и он видит его статус. Осталось добавить чекбокс в раннере. Для этого нужно нажать на карандаш рядом с именем раннера в гитлабе.



Все хорошо, теперь можно перейти к пайплайну.


Настройка пайплайна в .gitlab-ci.yml


Инструкции для раннера описываются в .gitlab-ci.yml. Полная документация описана здесь.


Первым делом нам нужно описать stages. Шаги пайплайна. Их будет 4. Каждый stage отдельный job, который будет выполнять раннер.


stages:  - testing # Запуск тестов  - history_copy # Копирование результата тестов из предыдущего запуска тестов  - reports # Генерация отчета  - deploy # Публикация отчета на gitlab pages

Шаг первый. Testing


docker_job: # Название job  stage: testing # Первый stage, который нужно выполнить  tags:    - docker # С помощью этого тега gitlab поймет, какой раннер нужно запустить. Он запустит докер контейнер, из образа, который мы указывали в 6 шаге регистрации раннера.  before_script:    - pip install -r requirements.txt # Устанавливаем пакеты в поднятом контейнере перед запуском самих тестов  script:    - pytest -n=4 --alluredir=./allure-results tests/test_dog_api.py # Запускаем тесты параллельно(-n=4 обеспечивает нам это), указав папку с результатами тестов через --alluredir=  allow_failure: true # Это позволит нам продолжить выполнение пайплайна в случае, если тесты упали.  artifacts: # Сущность, с помощью которой, мы сохраним результат тестирования.    when: always # Сохранять всегда    paths:      - ./allure-results # Здесь будет сохранен отчет    expire_in: 1 day # Да, он будет удален через день. Нет смысла хранить его в течение длительного срока.

Шаг второй. history_copy


history_job: # Название job  stage: history_copy # Это второй stage, который нужно выполнить  tags:    - docker # Пользуемся тем же самым раннером  image: storytel/alpine-bash-curl # Но теперь укажем раннеру использовать другой образ, для того чтобы скачать результаты теста из предыдущего пайплайна. Нам же нужна история тестов, верно?  script:    - 'curl --location --output artifacts.zip "https://gitlab.smarthead.ru/api/v4/projects/(АЙДИ ВАШЕГО РЕПОЗИТОРИЯ)/jobs/artifacts/master/download?job=pages&job_token=$CI_JOB_TOKEN"'  # С помощью api гитлаба скачиваем файлы из job, который будет указан ниже. Обратите внимание на текст на русском в ссылке. Очень важно указать вместо текста и скобок номер вашего репозиториия    - unzip artifacts.zip # Распаковываем файлы    - chmod -R 777 public # Даем права любые манипуляции с содержимым    - cp -r ./public/history ./allure-results # Копируем историю в папку с результатами теста  allow_failure: true # Так как при первом запуске пайплайна истории нет, это позволит нам избежать падения пайплайна. В дальнейшем эту строчку можно спокойно удалить.  artifacts:     paths:      - ./allure-results # Сохраняем данные    expire_in: 1 day  rules:    - when: always # Сохранять всегда

Шаг третий. reports


allure_job: # Название job  stage: reports # Третий stage, который будет выполнен  tags:    - docker # Пользуемся тем же самым раннером  image: frankescobar/allure-docker-service # Указываем раннеру использовать образ с allure. В нем мы будем генерировать отчет.  script:     - allure generate -c ./allure-results -o ./allure-report # Генерируем отчет из ./allure-results внутрь папки ./allure-report  artifacts:    paths:      - ./allure-results # Примонтируем две этих директории для получения результатов тестирования и генерации отчетов соответственно      - ./allure-report    expire_in: 1 day  rules:    - when: always

Шаг четвертый. deploy


pages: # Названием этой job говорим гитлабу, чтобы захостил результат у себя в pages  stage: deploy # Четвертый stage, который будет выполнен  script:    - mkdir public # Создаем папку public. По умолчанию гитлаб хостит в gitlab pages только из папки public    - mv ./allure-report/* public # Перемещаем в папку public сгенерированный отчет.  artifacts:    paths:      - public  rules:    - when: always

Финальный .gitlab-ci.yml


stages:  - testing # Запуск тестов  - history_copy # Копирование результата тестов из предыдущего запуска тестов  - reports # Генерация отчета  - deploy # Публикация отчета на gitlab pagesdocker_job: # Название job  stage: testing # Первый stage, который нужно выполнить  tags:    - docker # С помощью этого тега gitlab поймет, какой раннер нужно запустить. Он запустит докер контейнер, из образа, который мы указывали в 6 шаге регистрации раннера.  before_script:    - pip install -r requirements.txt # Устанавливаем пакеты в поднятом контейнере перед запуском самих тестов  script:    - pytest -n=4 --alluredir=./allure-results tests/test_dog_api.py # Запускаем тесты параллельно(-n=4 обеспечивает нам это), указав папку с результатами тестов через --alluredir=  allow_failure: true # Это позволит нам продолжить выполнение пайплайна в случае, если тесты упали.  artifacts: # Сущность, с помощью которой, мы сохраним результат тестирования.    when: always # Сохранять всегда    paths:      - ./allure-results # Здесь будет сохранен отчет    expire_in: 1 day # Да, он будет удален через день. Нет смысла хранить его в течение длительного срока.history_job: # Название job  stage: history_copy # Это второй stage, который нужно выполнить  tags:    - docker # Пользуемся тем же самым раннером  image: storytel/alpine-bash-curl # Но теперь укажем раннеру использовать другой образ, для того чтобы скачать результаты теста из предыдущего пайплайна. Нам же нужна история тестов, верно?  script:    - 'curl --location --output artifacts.zip "https://gitlab.smarthead.ru/api/v4/projects/(АЙДИ ВАШЕГО РЕПОЗИТОРИЯ)/jobs/artifacts/master/download?job=pages&job_token=$CI_JOB_TOKEN"'  # С помощью api гитлаба скачиваем файлы из job, который будет указан ниже. Обратите внимание на текст на русском в ссылке. Очень важно указать вместо текста и скобок номер вашего репозиториия    - unzip artifacts.zip # Распаковываем файлы    - chmod -R 777 public # Даем права любые манипуляции с содержимым    - cp -r ./public/history ./allure-results # Копируем историю в папку с результатами теста  allow_failure: true # Так как при первом запуске пайплайна истории нет, это позволит нам избежать падения пайплайна. В дальнейшем эту строчку можно спокойно удалить.  artifacts:     paths:      - ./allure-results # Сохраняем данные    expire_in: 1 day  rules:    - when: always # Сохранять всегдаallure_job: # Название job  stage: reports # Третий stage, который будет выполнен  tags:    - docker # Пользуемся тем же самым раннером  image: frankescobar/allure-docker-service # Указываем раннеру использовать образ с allure. В нем мы будем генерировать отчет.  script:     - allure generate -c ./allure-results -o ./allure-report # Генерируем отчет из ./allure-results внутрь папки ./allure-report  artifacts:    paths:      - ./allure-results # Примонтируем две этих директории для получения результатов тестирования и генерации отчетов соответственно      - ./allure-report    expire_in: 1 day  rules:    - when: alwayspages: # Названием этой job говорим гитлабу, чтобы захостил результат у себя в pages  stage: deploy # Четвертый stage, который будет выполнен  script:    - mkdir public # Создаем папку public. По умолчанию гитлаб хостит в gitlab pages только из папки public    - mv ./allure-report/* public # Перемещаем в папку public сгенерированный отчет.  artifacts:    paths:      - public  rules:    - when: always

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


Осталось отправить в репозиторий все необходимые файлы, а именно:


  1. conftest.py
  2. Директорию tests
  3. requirements.txt (Убедитесь, что там есть все нужные зависимости)
  4. .gitlab-ci.yml

Отправляем и смотрим что получилось.


В первый раз пайплайн будет запускаться немного дольше, чем в следующие разы. Потому что ему нужно будет скачать нужные образы, запустить их и все такое. Заходим в репозиторий и через сайдбар переходим в CI / CD -> Pipelines


Здесь вы сможете увидеть статус пайплайна, а также перейти к Ci линтеру, где можете проверить конкретно свой .gitlab-ci.yml.




Когда пайплайн пройдет, увидеть ссылку и сам отчет можно через Settings -> Pages. При первом использовании pages в репозитории для просмотра страницы может понадобиться до 30 минут. Проходим по ссылке и смотрим отчет.



Получаем результат. Советую побродить по отчету и посмотреть результаты.



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



Как видите второй шаг в этот раз выполнился без ошибок. Это потому, на stage history_job проблем с копированием истории предыдущих пайплайнов не возникло. Теперь посмотрим результат тестов.



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



Настройка пайплайна для UI тестов


Реализовать их можно с помощью специальной сущности гитлаба [services](http://personeltest.ru/aways/docs.gitlab.com/ee/ci/services/). Это ключевое слово для использования докер образов, которые будут запущены до хода шагов в script. Для UI тестов нужно добавить в job пайплайна следующее:


  services:    - selenium/standalone-chrome:latest

Так как нам нужно обращаться к контейнеру, который поднимет гитлаб по определенному url, то он каким-то образом назначает этому сервису url, по которому можно слать запросы. Логика здесь такая:


  • Все после : отбрасывается
  • Слеш / заменяется двойным подчеркиванием __ и создается главный алиас
  • Слеш / заменяется одиночным дефисом - и создается дополнительный алиас (Необходим Gitlab Runner v1.1.0 и выше)

И теперь нужно изменить в фикстуре executor (по умолчанию используется chromedriver на вашей машине, но сейчас мы все это запускаем в контейнерах) для запуска ui тестов:


browser = webdriver.Remote(command_executor="http://personeltest.ru/away/selenium__standalone-chrome:4444/wd/hub")

Обратите внимание, что было в .gitlab-ci.yml:
selenium/standalone-chrome:latest
И по какому адресу нужно обращаться фикстуре для запуска тестов:
selenium__standalone-chrome


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


chrome_job:  stage: testing  services:    - selenium/standalone-chrome  image: python:3.8  tags:    - docker  before_script:    - pip install -r requirements.txt  script:    - pytest --alluredir=./allure-results tests/  allow_failure: true  artifacts:    when: always    paths:      - ./allure-results    expire_in: 1 day

Полезные ссылки


Помимо ссылок в статье, хочется поделиться еще несколькими, которые немного разъяснять вам что такое allure и как писать gitlab ci.


Статья об allure на хабре
Введение в gitlab ci

Подробнее..

Robot Framework для автоматизации тестирования ограничения и плюшки

01.03.2021 14:22:08 | Автор: admin

В автоматизации тестирования я уже более 11 лет. Скажу сразу, что являюсь поклонником старомодного тестирования на Java и очень настороженно отношусь к различным готовым фреймворкам. Если вы придерживаетесь такого же мнения или только задумываетесь об использовании Robot Framework, в этой статье я постараюсь рассказать вам о его ограничениях и, конечно же, опишу все его достоинства.

Я столкнулся с Robot Framework около года назад. Перед нами стояла задача силами двух инженеров автоматизировать довольно большой объем тестов в сжатые сроки, т.к. ручная регрессия перестала влезать в разумные рамки. Сам проект связан с пожарной безопасностью. Тестировать предстояло Web-часть в трех браузерах и Mobile-часть на множестве iOS и Android телефонов и планшетов. Помимо этого, в наличии были тесты, которые взаимодействовали и с Web, и с Mobile. Конечно, это не ракету построить, но и не совсем тривиально. Честно скажу, я сопротивлялся, мы долго думали и в итоге, по совокупности внутренних и внешних факторов, выбрали Robot Framework.

Пара слов и картиночек для знакомства с Robot Framework

Прежде чем разбирать плюсы и минусы, давайте очень коротко поговорим о том, что же такое Robot Framework. Возможно, кто-то впервые видит это название.

Robot Framework это keyword-driven фреймворк, разработанный специально для автоматизации тестирования. Он написан на Python, но для написания тестов обычно достаточно использовать готовые ключевые слова (кейворды), заложенные в этом фреймворке, не прибегая к программированию на Python. Нужно лишь загрузить необходимые библиотеки, например, SeleniumLibrary, и можно писать тест. В этой статье я дам общее представление о Robot Framework, но если после прочтения вы захотите углубиться в тему, то советую обратиться к официальной документации. В конце статьи также приведены ссылки на популярные библиотеки.

Что ж, перейдем к картиночкам. Вот так может выглядеть простой проект в IDE (на примере всеми любимой Википедии):

  • Синий и зеленый папки с файлами для описания страниц и тестов соответственно. Так можно реализовать page object паттерн.

  • Коричневый драйвера для различных браузеров.

  • Красный тело теста.

  • Желтый консоль, из которой можно запускать тесты и видеть консольные сообщения (полноценные логи не тут, но об этом позже).

Как видно, в тесте сплошные обертки в стиле BDD (можно не применять такой синтаксис, но лично мне он тут кажется удобным). Имплементация находится в объектах страниц, например:

В стандартной секции Settings мы видим подгрузку библиотеки для работы с Selenium, а в другой стандартной секции Keywords находятся имплементации наших самописных ключевых слов.

Думаю, для получения общего представления этого достаточно. Детальное описание работы с Robot Framework лежит за рамками моего поста

Плюсы и минусы

В этой части я расскажу о плюсах и минусах Robot Framework, с которыми столкнулась наша команда. Учитывая специфику задач на проекте, наверняка у вас будут и свои подводные камни. Я буду перечислять, двигаясь, на мой взгляд, от более значимого достоинства/недостатка к менее значимому.

Плюшки

Низкий порог входа

Как я уже писал выше, Robot Framework является keyword-driven фреймворком, а не языком программирования. Хоть синтаксис и схож с Python, знаний программирования требуется несколько меньше или, скажем так, их применение не обязательно там, где это позволяет сложность самой задачи. Однако, при необходимости можно пользоваться переменными, циклами, функциями, возвращающими значения, и т.п. Ближайшими альтернативами могут показаться Pytest и Selenide, но они требуют большей подготовки пользователя, нежели Robot Framework. Например, одной из встроенных стандартных библиотек является BuiltIn. Там вы можете найти такие кейворды как Sleep, Log, Run Keyword If, Should Be Equal As Strings и т.п. и написать что-то вроде:

Run Keyword If '${status}' == 'PASS' SomeAction

Поддержка Web и Mobile

Robot Framework неплохо работает в связке Mobile+Web (как end-to-end, так и атомарные тесты).

Наши Web тесты работают с Chrome, FF и IE. Мобильная часть работает как с локальными реальными устройствами на Android и iOS, так и с устройствами с фермы SauceLabs. Ограничение реальное локальное iOS-устройство можно тестировать только с Mac. И вообще iOS требует гораздо больше внимания, ведь тот же веб-драйвер для него надо пересобирать самостоятельно. (Тестирование iOS это отдельная большая тема, и если интересно, дайте знать в комментариях, мне есть о чем рассказать)

Тэги

Есть возможность задавать тестам тэги. Тэгами может быть любая информация, которая пригодится нам для идентификации теста: ID теста, список компонент, к которым относится тест, и т.п. Этим мы обеспечиваем связь тестов с тестами или требованиями (traceability) и задаем необходимую информацию для конфигурирования запуска тестов. Указав в запускалке один тэг, мы можем запустить все тесты, которые относятся к определенному компоненту, или же можем при запуске явным образом перечислить тест-кейсы, которые надо запустить (удобно при регрессионном тестировании). Подробнее про тэги по ссылке.

Хорошие отчеты из коробки

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

  • Output.xml результаты тестов в формате XML. Пригодятся для мерджа результатов командой rebot. Пример:

  • Log.html подробные результаты в HTML-формате. Полезны больше для разработчиков тестов. Пример:

  • Report.html высокоуровневые результаты без подробной детализации. Полезны для демонстрации людям со стороны и менеджменту. Пример:

BDD из коробки

Синтаксис Gherkin языка с его нотациями Given, When, Then и And включен по умолчанию, и любой шаг может быть записан как в этой нотации, так и без нее. Можно использовать нотации или нет тесты просто игнорируют их. К примеру, эти два кейворда с точки зрения фреймворка идентичны:

Welcome page should be open

And welcome page should be open

Подробнее по ссылке.

Page Object паттерн

Robot Framework позволяет реализовать Page Object паттерн не при помощи ООП, а при помощи синтаксиса ключевых слов. Смысл в том, чтобы последовательно в кейворде указывать, с какой страницей мы работаем -> с какой областью внутри нее мы работаем -> с каким контролом работаем и что мы с ним делаем. Пример:

On Main page on Users tab I click Create user icon

где кейворд On Main page on Users tab I click Create user icon хранится в отдельном робот файлике, скажем, с названием mainPage.robot. Этот файлик мы подгружаем в наш файл с тестами по необходимости.

См. также пример из секции Пара слов и картиночек для знакомства с Robot Framework.

Параллельный запуск

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

Грабли

Отсутствует возможность отладки встроенными средствами

Имеется ввиду классическая расстановка брейкпоинтов. Приходится либо выводить что-то дополнительное в лог, либо ставить временные слипы и так обходить эту проблему. В сети описаны некоторые способы прикрутить дебаг, но для уровня целевой аудитории Robot Framework это сложновато.

Не поддерживается AWS

AWS (Amazon Web Services коммерческое публичное облако, ферма мобильных устройств) не поддерживает тесты на Robot Framework. AWS работает таким образом, что код исполняется на стороне Amazon, и тесты в формате Robot Framework не допустимы. Зато другая ферма, SauceLabs, устроена по другому принципу и прекрасно работает с Robot Framework (есть проблемы с администрированием их сервиса из России, но они решаются общением со службой поддержки или работой под VPN).

IDE сложности

RIDE (Robot IDE), специальная IDE для Robot Framework, мягко говоря, сырая. Режим работы в табличном виде (как раз для воплощения идеи keyword-driven фреймворка) выглядит так:

Режим работы в редакторе текста:

Ни в одном из двух предлагаемых режимов работать невозможно. Приложение периодически падает (хотя на других проектах такого нет). В режиме текста нет элементарного Go to Definition. В режиме таблиц Go to Definition есть, но сам этот режим крайне неудобен для средних и больших проектов.

PyCharm работает лучше, но, к сожалению, существующие плагины не справляются с автокомплитом некоторых библиотек (например, SeleniumLibrary)

Плохая поддержка сторонних библиотек

Готовые, уже существующие в сети библиотеки зачастую не поддерживаются. Пользователей мало, и они переходят в разряд зомби. Например, работа с почтой, сравнение скриншотов и т.п. Можно, конечно, написать свои библиотеки на чистом Python (и Robot Framework это позволяет), но смысла в такой схеме остается мало.

Справедливости ради стоит сказать, что нам для наших целей не пришлось писать ничего на Python и хватило добытых не без труда готовых библиотек.

Выводы

Выбор инструмента Robot Framework для нашего проекта был абсолютно верным и позволил выполнить наши обязательства в срок и с надлежащим качеством. Однако, надо понимать, что это, конечно же, не серебряная пуля, есть много но, которые надо иметь в виду.

Инструмент это всего лишь средство для достижения поставленной цели, и не всегда надо микроскопом забивать гвозди, даже если это выглядит эффектно. Ругать или нахваливать Robot Framework я не берусь. Скажу лишь, что это хороший инструмент для своих целей

Полезные ссылки

  • Robot Framework User Guide: http://robotframework.org/robotframework/latest/RobotFrameworkUserGuide.html

  • SeleniumLibrary: https://robotframework.org/SeleniumLibrary/SeleniumLibrary.html

  • BuiltIn: http://robotframework.org/robotframework/latest/libraries/BuiltIn.html

  • AppiumLibrary: http://serhatbolsu.github.io/robotframework-appiumlibrary/AppiumLibrary.html

  • Collections: https://robotframework.org/robotframework/latest/libraries/Collections.html

Подробнее..

Погружение в автотестирование на iOS. Часть 2. Как взаимодействовать с ui-элементами iOS приложения в тестах

25.01.2021 06:07:56 | Автор: admin

Привет, Хабр!

В прошлой статье мы разобрались:

  • Что такое ui-тесты и для чего они нужны;

  • Как настроить окружение для тестов;

  • Как находить ui-элементы в проекте и проставлять им accessibilityidentifier.

В этой статье мы разберем:

  1. Как обращаться и инициализировать ui-элементы в ваших тестах;

  2. Как взаимодействовать с ui-элементами приложения;

  3. Как писать ассерты для проверки в автотесте ожидаемого результата.

Как обращаться и инициализировать ui-элементы

При наличии айдишника у ui-элемента, достаточно указать его при обращении.

XCUIApplication().buttons["Help"]

Если же у вас нет id у элемента, есть способ найти его при помощи XCUIElementQuery. Этот класс позволяет искать элемент несколькими способами.

// Находит все кнопки внутри scroll view (отобразит кнопки только прямого потомка scroll view)XCUIApplication().scrollViews["Main"].children(matching: .button)// Находит все кнопки внутри scroll view (отобразит кнопки прямого потомка scroll view, но также и его потомков)XCUIApplication().scrollViews["Main"].descendants(matching: .button)    // Находит четвертую кнопку на экранеXCUIApplication().buttons.element(boundBy: 3)// Находит в scroll view ui-элемент содержащий label = identifierXCUIApplication().scrollViews["Main"].containing(NSPredicate(format: "label == %@","identifier").element// Находит первую кнопку на экранеXCUIApplication().buttons.firstMatch

Немного про NSPredicate это класс, который позволяет фильтровать объекты по нужному вам условию. Статья с хорошим объяснением как использовать NSPredicate.

Пример иницилизации переменной:

let moneyTitle: XCUIElement = XCUIApplication().staticTexts["accessibilityID"]

Взаимодействия с ui-элементами приложения

Нажатие и удержание

Вы можете в своих тестах совершать: нажатие, удержание и drag&drop ui-элементов.

Перечень методов можно посмотреть здесь, раздел Tapping and Pressing.

// Совершаем нажатие на ui-элементXCUIApplication().buttons.element.tap()// Cовершаем двойное нажатие на ui-элементXCUIApplication().buttons.element.doubleTap()// Удерживаем нажатие в течение времени, которое передали в forDurationXCUIApplication().buttons.element.press(forDuration: 3)// Совершаем нажатие на ui-элемент и затем перетаскиваем его к другому ui-элементуXCUIApplication().buttons.element.press(forDuration: 3, thenDragTo: XCUIApplication().searchFields.element)

Ввод текста

Вы можете вводить текст по букве обращаясь к системной клавиатуре:

XCUIApplication().textFields.element.tap()XCUIApplication().keys["h"].tap()XCUIApplication().keys["e"].tap()XCUIApplication().keys["l"].tap()XCUIApplication().keys["p"].tap()

Либо вводить целую строку:

XCUIApplication().textFields.element.typeText("help")

Информация по методу typeText

Множественные нажатия

Вы можете совершать множественные нажатия в своих тестах.

// Совершаем нажатие двумя пальцами на ui-элементXCUIApplication().buttons.element.twoFingerTap()/*     Совершаем нажатие на элемент столько раз сколько передали     в withNumberOfTaps и столькими "пальцами" сколько передали     в numberOfTouches*/XCUIApplication().buttons.element.tap(withNumberOfTaps: 1, numberOfTouches: 1)

Перечень методов можно посмотреть здесь, раздел Multiple Taps.

Жесты

Вы можете совершать разные жесты в своих тестах.

// Совершаем свайп в указанном направленииswipeLeft()swipeRight()swipeUp()swipeDown()// Совершаем свайп в указанном направлении с заданной скоростьюswipeLeft(velocity: 0.5)swipeRight(velocity: 0.5)swipeUp(velocity: 0.5)swipeDown(velocity: 0.5)// Совершаем приближения ui-элемента (withScale указываем больше 1)XCUIApplication().images.element(boundBy: 0).pinch(withScale: 2, velocity: 1) // Совершаем отдаления ui-элемента (withScale указываем от 0 до 1)XCUIApplication().images.element(boundBy: 0).pinch(withScale: 0.5, velocity: 1)// Совершаем вращение ui-элементаXCUIApplication().images.element(boundBy: 0).rotate(0.5, withVelocity: 0.5)

Перечень методов можно посмотреть здесь, раздел Performing Gestures.

Взаимодействие с UISlider

UISlider это элемент управления для выбора одного значения из диапазона значений.

Когда мы хотим изменить положение ползунка в слайдере, мы не передаем значение, которое хотим установить. Вместо этого мы выбираем число в диапазоне от 0 до 1. Где 0 это минимальное значение в слайдере, а 1 максимальное. Представим, что у нас есть слайдер с максимальным значением 100 и нам нужно сдвинуть ползунок на значение 25. Это будет выглядеть так:

XCUIApplication().sliders.element.adjust(toNormalizedSliderPosition: 0.25)

Взаимодействие с UIPickerView и UIDatePicker

UIPickerView и UIDatePicker это ui-элементы, которые используют "колесики" для выбора необходимых значений.

XCUIElement имеет специальный метод для взаимодействия с UIPickerView и UIDatePicker:

  • Для пикеров с одним колесом, мы можем получить доступ через element(), и указать значение, которое хотим выбрать;

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

// Пикер с одним колесомXCUIApplication().pickerWheels.element.adjust(toPickerWheelValue: "BMW")// Пикер с несколькими колесамиXCUIApplication().pickerWheels.elementBoundByIndex(0).adjust(toPickerWheelValue: "BMW")XCUIApplication().pickerWheels.elementBoundByIndex(1).adjust(toPickerWheelValue: "X6")

Взаимодействие с системным алертом

Системный алерт это объект, отображающий предупреждающее сообщение для пользователя.

Чтобы взаимодействовать с ним, вам понадобится использовать метод addUIInterruptionMonitor(withDescription:handler:)

Где вы передаете:

  • withDescription заголовок алерта;

  • handler - действие, которое хотите совершить.

Пример использования в тестах:

addUIInterruptionMonitor(withDescription: "Current Location Not Available") { alert in    alert.buttons["OK"].tap()    return true}

Взаимодействие с Navigation Bar

Navigation bar это панель навигации, отображается в верхней части экрана приложения под status bar и позволяет перемещаться по приложению.

Представим, что у нас есть две кнопки и текст по середине в Navigation Bar.

Вот пример того как можно их иницилизировать и в дальнейшем с ними взаимодействовать:

// Иницилизируем крайнюю левую кнопку в Navigation bar let leftNavBarButton = XCUIApplication().navigationBars.children(matching: .button).firstMatch// Иницилизируем тест посередине в Navigation bar let topicNavBar = XCUIApplication().navigationBars.children(matching: .staticTexts).firstMatch// Иницилизируем крайнюю правую кнопку в Navigation barlet rightNavBarButton = XCUIApplication().navigationBars.children(matching: .button).element(boundBy: 1)// Нажимаем на кнопки в Navigation bar leftNavBarButton.tap()rightNavBarButton.tap()// Проверяем заголовок в Navigation bar XCTAssertEqual(topicNavBar.title, "Topic")

Взаимодействие с Tab bar

Tab bar это панель вкладок, отображается в нижней части экрана приложения. Она даёт возможность быстро переключаться между различными разделами приложения.

Для переключения между вкладками достаточно тапать на индекс элемента в Tab bar.

// Открываем первую вкладку XCUIApplication().tabBars.buttons.element(boundBy: 0)// Открываем третью вкладкуXCUIApplication().tabBars.buttons.element(boundBy: 2)

Создание ассертов:

Ассерты это проверки необходимого условия.

Рассмотрим несколько вариантов их использования:

// Ассерт, что кнопка отображается на экранеXCTAssertTrue(XCUIApplication().buttons["Warning"].exists)// Ассерт, что кнопка не выделенаXCTAssertFalse(XCUIApplication().buttons["Warning"].isSelected)// Ассерт, что title кнопки равен - BuyXCTAssertEqual(XCUIApplication().buttons.element.title, "Buy")// Ассерт, что placeholder в textFields не равен - placeHolderXCTAssertNotEqual(XCUIApplication().textFields.element.placeholderValue, "placeHolder")// Ассерт, что value в textFields равно - valueXCTAssertEqual(XCUIApplication().textFields.element.value, "value")

Полный перечень возможных ассертов можно посмотреть здесь, раздел Test Assertions

Перечень возможных атрибутов ui-элементов можно посмотреть здесь

Заключение:

Взаимодействовать с ui-элементами во время теста не так сложно, как кажется на первый взгляд. Воспользовавшись примерами выше, можно быстро добавить необходимые методы в свой проект с ui-тестами.

В следующей статье мы расскажем про жизненый цикл тестового приложения:

  • Как делать предусловия и послеусловия;

  • Как сбрасывать статус пермишенов приложения перед запуском тестов (доступ к галерее, фото и так далее);

  • Как запускать приложения по bundle identifier (например запуск сафари, документов и так далее);

  • И многое другое.

Подробнее..

Из песочницы Автоматизация системных тестов на базе QEMU (Часть 12)

23.09.2020 12:10:47 | Автор: admin

Эта статья посвящена автоматизации системного (end-to-end) тестирования с использованием виртуальных машин. В статье рассматриваются такие вопросы, как автоматизация развертывания и настройки виртуальных стендов, а также автоматизация запуска процессов внутри виртуальных машин с последующим контролем полученных результатов. В конце статьи мы получим пусть неидеальный (к этому мы ещё вернемся), но простой и понятный скрипт, с помощью которого вы сможете запускать системные тесты одной кнопкой, даже не имея у себя на компьютере ни одной виртуальной машины.


Статья предполагает наличие следующих навыков у читателя:


  • Уверенное пользование ОС семейства Linux;
  • Базовое понимание принципов виртуализации;
  • Знакомство с гипервизором QEMU и графическим клиентом virt-manager

Статья разбита на две части: в первой части мы познакомимся с основными инструментами, которые позволят нам создавать, развертывать и управлять виртуальными машинами используя исключительно командную строку. Эти знания нам пригодятся для второй части статьи, где мы соединим эти инструменты вместе и попробуем автоматизировать тесты конкретного сетевого приложения.


Дисклеймер

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


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


Что такое системное тестирование


Системное тестирование (или, как его ещё иногда называют, end-to-end тестирование) это тестирование программы (или даже целой системы) с учётом окружения, в которой программе придётся работать. Помимо системного тестирования также существует unit-тестирование (тестирование конкретных функций и модулей), интеграционное тестирование (тестирование больших самостоятельных компонент программы или программы целиком) и ещё много других видов тестирования. Но почему же нас может заинтересовать именно системное?


Наверное, у всех бывали ситуации, когда у разработчика всё работает, а у заказчика вечно что-то ломается. Так вот, системное тестирование хорошо тем, что программа проверяется именно в том виде, в котором её увидит конечный потребитель. Системные тесты позволяют воспроизвести некоторое окружение и проверить, как программа с этим окружение справляется. Вот некоторые примеры настраиваемого окружения:


  • Вид и версия ОС;
  • Разные настройки ОС (например, в области разграничения прав доступа);
  • Наличие или отсутствие определенных программ в ОС (в т.ч. драйверов);
  • Наличие сети и других компьютеров в ней;
  • Количество оперативной памяти или определённая архитектура процессора.

Довольно часто разработчики и тестировщики просто не знают, как подступиться к задаче автоматизации системных тестов, либо им кажется, что затраты на автоматизацию не окупят себя. Этой статьёй я хотел бы продемонстрировать, что автоматизация системных тестов не настолько сложна, как это может показаться, и я надеюсь, что она послужит для кого-то хорошей отправной точкой.


Почему именно виртуалки?


Может возникнуть вопрос, а зачем использовать именно виртуалки? Ведь для воспроизведения окружения для программы можно использовать и контейнеры (например, если Вас интересует поведение программы в связке с другими программами). И действительно, для контейнеров существует несколько решений по автоматизации тестов. Однако, существуют классы сценариев, которые нельзя протестировать с помощью контейнеров:


  1. Нельзя протестировать приложения не под Linux, а также гетерогенные стенды из нескольких машин;
  2. Нельзя протестировать GUI (или даже псевдо-GUI);
  3. Нельзя протестировать драйвера и работу с оборудованием.

Автоматизация пунктов 1 и 2 потребует от нас существенной подготовки, поэтому оставим их для одной из следующих статей. А вот для пункта 3 вполне хватит той информации, которую Вы узнаете из этой статьи. В этой статье (во второй её части) мы как раз продемонстрируем автоматизацию тестирования приложения, которое взаимодействует с оборудованием напрямую, в обход операционной системы.


Какой выбрать гипервизор?


В целом, схема, которую мы предложим в этой статье, применима к любому гипервизору. Но мы воспользуемся QEMU, потому что он позволит нам получить работающий прототип затратив при этом минимальные усилия и время. Основные принципы и идеи из этой статьи, при желании, можно реализовать и на других гипервизорах (например, на VirtualBox).


Какие действия мы хотим автоматизировать?


Для того, чтобы автоматизировать системные тесты, нам нужно решить как минимум следующие вопросы:


  1. Как автоматически создать виртуалку;
  2. Как "раскатать" и настроить ОС;
  3. Как научиться управлять виртуалкой после её установки. В частности, нас интересует, как запускать процессы внутри виртуалки и копировать на неё файлы.

Заметка про тестирование методом чёрного ящика

По-хорошему, настоящая автоматизация системных тестов предполагает, что все эти действия мы будем выполнять так же, как и тестировщик, выполняющий ручное тестирование: нам нужно нажимать клавиши на виртуальной клавиатуре, двигать виртуальной мышью и анализировать происходящее на виртуальном экране. Такой подход позволяет автоматизировать любые тестовые сценарии, поскольку не делает никаких предположений о том, как устроена внутри тестируемая система. Однако, это очень сложный путь автоматизации. Мы, пока что, воспользуемся менее универсальным, но гораздо более простым способом.


Что ж, погнали!


Создаем виртуалку


Конечно, я думаю все пробовали создавать виртуальные машины вручную. Для этого в графическом интерфейсе рисуются всякие диалоговые окна, где надо выбрать конфигурацию, диски, сетевые адаптеры и прочее прочее. Так вот, для гипервизора QEMU всё это можно сделать и в консольном режиме с помощью утилиты virt-install. Давайте посмотрим как будет выглядеть простейшее создание виртуальной машины с помощью этой утилиты:


virt-install \    --name my_super_vm \    --ram 1024 \    --disk my_super_vm.qcow2,size=8 \    --cdrom /path/to/ubuntu_server.iso

Вот такой простой командой мы можем создать виртуальную машину с именем my_super_vm, 1024 Мегабайтами оперативной памяти, новым диском my_super_vm.qcow2 размером 8 Гигабайт. В виртуальном CD-приводе такой машины будет смонтирован установочный образ ubuntu_server.iso (конечно, этот образ надо предварительно скачать), который, как обычно, нужен для установки ОС на свежесозданную виртуалку.


Впрочем, если Вы выполните такую команду, то увидите как у вас запустилось графическое окно с VNC-клиентом, который подключается к виртуальной консоли виртуальной машины. В этом окне Вы увидите начало установки Ubuntu Server 18.04. Конечно, реальный человек бы смог протыкать несколько клавиш на клавиатуре и установить Ubuntu Server, но мы лишены такой роскоши, ведь мы работаем исключительно консольными командами.


Но наше положение не такое уж и безвыходное, ведь установить ОС на виртуальную машину можно и по-другому.


Создаём диск из шаблона


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


Вызов команды virt-install, приведённый выше, выполняет сразу два действия: он создаёт одновременно и машину и диск. Из-за может сложиться неверное впечатление, что эти сущности неотделимы друг от друга.


Но на самом деле никто не мешает нам отдельно создать виртуальный диск, а затем подключить его к виртуальной машине. Вы можете подготовить свой образ диска, но в этой статье мы для простоты воспользуемся уже готовым. Проект libguestfs поддерживает в свободном доступе целый репозиторий таких образов. Кроме того, в рамках этого же проекта существует утилита virt-builder, которая позволяет легко скачать нужный образ диска и доработать его "напильником" под наши нужды.


Несколько слов про проект libguestfs и почему стоит обратить на него внимание

В первую очередь Libguestfs это программная библиотека, которая позволяет легко манипулировать содержимым образом виртуальных дисков. Под манипуляцией подразумеваются такие операции как форматирование дисков, копирование файлов на диск и из него и так далее. Кроме того, в рамках проекта реализован целый набор утилит на все случаи жизни, которые используют различные возможности библиотеки. Среди них есть очень простые утилиты (в стиле Unix-way), которые выполняют всего одно действие, например virt-copy-in. С другой стороны есть команды-комбайны, которые умеют много всего и сразу, такие как virt-builder.


Итак, что же нам позволяет сделать virt-builder? С помощью этой утилиты мы можем на лету сформировать уже "подготовленный" диск с уже установленной Ubuntu Server. Сделать это можно так:


virt-builder ubuntu-18.04 \    --format qcow2 \    --output my_super_disk.qcow2

Что же здесь происходит? Мы говорим, что хотели бы создать диск в формате qcow2 (можно выбрать другой) и использовать в качестве основы шаблон ubuntu-18.04, который хранится в репозитории шаблонов в проекте libguestfs. Команда virt-builder скачает этот шаблон из Интернета и затем на его основе сгенерирует итоговый диск, в котором теперь будет установлен Ubuntu Server!


Кешируемость шаблонов

Приятной особенностью утилиты virt-builder является то, что шаблон диска кешируется после первого скачивания, поэтому последующие создания дисков будут проходить гораздо быстрее


Ну а теперь нам остаётся создать виртуальную машину, и указать в качестве диска (импортировать) подготовленный образ my_super_disk.qcow2:


virt-install \    --import \    --name my_super_vm \    --ram 1024 \    --disk my_super_vm.qcow2

Обратите внимание, что исчез параметр --cdrom, он нам больше не нужен. Также добавился параметр --import. Этот параметр указывает, что виртуалка будет загружаться не с cdrom, а с виртуального диска (то есть это влияет на Bios Boot Options виртуальной машины). Ну а т.к. диск у нас теперь содержит установленный Ubuntu Server, такая загрузка пройдёт успешно.


Попробуйте выполнить эти команды и создать таким образом виртуальную машину my_super_vm. Вы сможете убедиться, что Ubuntu Server 18.04 действительно уже установлен и успешно загружается.


Соединяем сетью виртуалку и хост


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


Ещё раз хотелось бы отметить, что эталоном автоматизации системных тестов должна выступать автоматизация действий человека: как он нажимает на клавиши, кликает мышкой и совершает другие действия. И ещё раз напомним, что это очень сложный (хоть и правильный) путь. Мы же воспользуемся тем фактом, что нам не требуется взаимодействовать с GUI, нам пока вполне хватит возможности выполнения произвольных bash-команд на гостевой системе.


Для этого существуют разные подходы, но мы с Вами в этой статье рассмотрим только самый простой и, в некоторой степени, топорный вариант: установка SSH-соединения между хостом и виртуалкой.


Альтернативные подходы

Не всегда канал управления виртуалкой через ssh является приемлемым вариантом. Такое бывает, если Вы тестируете приложение, которое каким-то образом завязано на сетевую подсистему. Например, в случае если вы как раз таки и разрабатываете ssh-сервер. Вы же не хотите, чтобы Ваши тесты полагались на работоспособность ещё неготовой программы? Либо Вы хотите проверить, как поведёт себя Ваша программа на виртуалке, где вообще нет сетевых интерфейсов.


В качестве альтернативы можно управлять гостевой системой через последовательный порт. При этом последовательный порт образует как бы трубу (pipe), проложенную между хостом и виртуалкой. Со стороны Linux-хоста эта труба видна как unix-socket, а со стороны гостевой системы как последовательное устройство. Всё, что отправляется в один конец трубы, тут же появляется из другого. Соответственно, как и в случае с ssh, на гостевой системе должен работать некий сервер, ожидающий получения данных из последовательного порта. Примером такого сервера может служить qemu-guest-agent.


В случае, если Вы работаете с гипервизором Hyper-V, то помимо последовательного порта можно попробовать воспользоваться механизмом KVP (Key-Value Pairs) или Hyper-V Sockets.


Для этого нам потребуется сделать несколько манипуляций:


  1. Создать виртуальную сеть между хостом и виртуалкой;
  2. Настроить сетевой интерфейс на виртуалке;
  3. Задать пароль от пользователя root на виртуалке;
  4. Подредактировать SSH-настройке на виртуалке, чтобы можно было соединяться по SSH от имени рута с помощью пароля.

По поводу ssh без пароля

Лучше, конечно, было бы настроить вход по ключу. Это позволило бы немного упростить финальный скрипт. Единственная причина, почему я не стал так делать это скользкий момент, связанный с sudo. Но вообще принципиальных препятствий никаких нет.


Для начала займемся созданием виртальной сети между хостом и виртуалкой. Для этого нам надо сделать две простых операции:


  1. Создать виртуальную сеть;
  2. Подключить виртуалку к этой сети (хост уже будет подключен к сети по-умолчанию).

Итак, поехали.


Для создания виртуальной сети мы воспользуемся ещё одной утилитой virsh, которая работает поверх ещё одной замечательной библиотеки libvirt.


Несколько слов про libvirt

Libvirt это, опять же, программная библиотека, предназначенная для управления гипервизорами. Проект libvirt ставит перед собой задачу объять необъятное сделать интерфейс управления, подходящий сразу для всех гипервизоров. Задача благая, но уж слишком амбициозная. На данный момент гипервизоры, отличные от QEMU, поддерживаются довольно слабо. Однако, если Вам требуется сделать что-то именно с QEMU, то проект libvirt и утилита virsh, входящая в его состав, являются самым быстрым и удобным вариантом.


В libvirt для создания различных виртуальных объектов (дисков, сетей, виртуалок) используются XML-схемы. XML-схема для создания сети выглядит примерно так:


<network>    <name>net_for_ssh</name>    <bridge name='net_for_ssh'/>    <ip address='192.168.100.1' netmask='255.255.255.0'/></network>

Где 192.168.100.1 это адрес, присваиваемый сетевому интерфейсу, который будет автоматически создан на хосте и ассоциирован с новой виртуальной сетью.


Для того, чтобы создать сеть необходимо передать путь к файлу с xml схемой сети в следующую команду:


virsh net-define net_for_ssh.xml

После создания сеть создаётся в выключенном состоянии, поэтому её ещё надо включить:


virsh net-start net_for_ssh

Теперь надо подключить виртуалку к свежесозданной сети. Для этого при создании виртуалки необходимо добавить параметр --network:


virt-install \    --import \    --name my_super_vm \    --ram 1024 \    --disk my_super_vm.qcow2 \    --network network=net_for_ssh \    --noautoconsole

Обратите также внимание на аргумент --noautoconsole, который отключает автоматическое подключение к консоли виртуалки через VNC-клиент (впрочем, если Вам всё-равно хочется зайти посмотреть на виртуалку, Вы можете воспользоваться virt-manager).


Соединение готово, но пропинговать с хоста нашу виртуалку мы всё ещё не можем: сетевой интерфейс пока не настроен.


Настройка сетевого интерфейса на виртуалке


Как же мы будем настраивать интерфейс внутри виртуалки, если мы пока не можем выполнять на ней никаких команд (SSH-канал ведь ещё не настроен)? В этом нам снова поможет библиотека libguestfs и утилита virt-builder.


Дело в том, эта библиотека позволяет, в числе прочего, копировать файлы на виртуальный диск. Как мы знаем, в Ubuntu Server 18.04 за сетевые настройки отвечает netplan, и для того, чтобы сконфигурировать сетевой интерфейс, нам достаточно подложить специальный .yaml файлик в каталог /etc/netplan. И сделать это можно с помощью той же утилиты virt-builder с помощью параметра --copy-in:


Файл с настройками сетевого интерфейса netcfg_ssh.yaml
network:  version: 2  renderer: networkd  ethernets:    ens3:      addresses:        - 192.168.100.2/24

virt-builder ubuntu-18.04 \    --format qcow2 \    --output my_super_disk.qcow2 \    --copy-in netcfg_ssh.yaml:/etc/netplan/

И теперь при создании виртуального диска после раскатывания Ubuntu Server 18.04 из шаблона virt-builder дополнительно скопирует файл netcfg_ssh.yaml и подложит его в директорию /etc/netplan/ на файловой системе виртуального диска.


Теперь виртуальная машина должна пинговаться, проверим:


ping 192.168.100.2 -c5

Почти всё сделали, осталось лишь настроить SSH.


Настраиваем SSH на виртуалке


Для создания канала управления виртуалкой осталось сделать совсем чуть чуть:


  1. Прописать пароль для root-пользователя в виртуалке;
  2. Создать ключи на виртуалке для SSH-сервера чтобы он стартовал без ругани;
  3. Прописать в конфиг SSH-сервера возможность подключаться от имени рута с использованием пароля.

Начнём с пароля для root. Здесь нас снова выручает virt-builder, который, на самом деле, позволяет Вам делать с виртуальным диском поистине удивительные вещи, одна из которых прописать пароль для root-пользоавтеля:


virt-builder ubuntu-18.04 \    --format qcow2 \    --output my_super_disk.qcow2 \    --root-password password:1111 \    --copy-in netcfg_ssh.yaml:/etc/netplan/

Теперь осталось сгенерировать ключи для SSH и подправить конфиг. Для этого нам надо всего-то надо выполнить пару команд:


ssh-keygen -Ased -i \"s/.*PermitRootLogin.*/PermitRootLogin yes/g\" /etc/ssh/sshd_config

Вот только вопрос, а как же нам выполнить эти команды? Ведь virt-builder работает только с образами дисков, никаких виртуальных машин на этом этапе не создаётся. Однако, уvirt-builder есть ещё пара козырей в рукаве. Он позволяет запускать программы гостевой системы без запуска собственно гостевой системы с помощью параметра --run-command:


virt-builder ubuntu-18.04 \    --format qcow2 \    --output my_super_disk.qcow2 \    --root-password password:1111 \    --run-command "ssh-keygen -A" \    --run-command "sed -i \"s/.*PermitRootLogin.*/PermitRootLogin yes/g\" /etc/ssh/sshd_config" \    --copy-in netcfg_ssh.yaml:/etc/netplan/

А как это работает?

Хороший вопрос. Документация libguestfs ничего не говорит нам о деталях реализации этого механизма, а исходники довольно запутаны. Но похоже, что здесь используется User Space Linux Kernel. Это не контейнеры, но очень на них похоже. В любом случае можно сделать следующие выводы:


  • в параметре --run-command можно смело вызвать любой бинарник, который есть на диске гостевой системы
  • при этом можно смело пользоваться сетью, например устанавливать пакеты с помощью apt install

И теперь всё! Канал настроен! Можно было бы даже попробовать подключиться, если бы не одно "но". После выполнения команды virt-install виртуалка только только начинает включаться. Между моментом включения виртуалки и запуском на ней ssh сервера может пройти несколько секунд. Поэтому нам потребуется организовать незатейливый механизм ожидания ssh сервера:


#!/bin/bashSSH_CMD="sshpass -p 1111 ssh -o StrictHostKeyChecking=no"while ! $SSH_CMD root@192.168.100.2 echo Hello world from my super vm!do    echo "Waiting for client VM ..."    sleep 1done

Также, мне пришлось добавить параметр -o StrictHostKeyChecking=no к команде ssh для того, чтобы запрос на добавление сервера в список доверенных серверов не блокировал выполнение скрипта. Так же мне пришлось воспользоваться утилитой sshpass для того, чтобы автоматизировать ввод пароля.


Итоги


В первой части статьи мы пока не написали ни одного реального системного теста, зато познакомились с серьёзным арсеналом утилит по работе с виртуальными машинами, научились автоматически создавать виртуалки, раскатывать на них ОС, настраивать их, а также налаживать канал управления по SSH. С этим запасом знаний мы теперь можем смело переходить к самому главному и интересному: как же, всё-таки, автоматизировать системные тесты на виртуалках.

Подробнее..

Автоматизация системных тестов на базе QEMU (Часть 22)

26.09.2020 12:23:55 | Автор: admin

Это вторая часть статьи, посвященной автоматизации системного тестирования на основе виртуальных машин. Первую часть можно найти здесь: http://personeltest.ru/aways/habr.com/ru/post/520310/


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


Дисклеймер

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


Итак, в прошлой части мы запаслись внушительным арсеналом из навыков работы с виртуальными машинами из командной строки: научились устанавливать виртуалки, раскатывать на них ОС (на примере Ubuntu Server 18.04), соединять виртуалку с хостом по сети и даже организовывать канал управления через SSH. Всё это нам пригодится в этой статье, но прежде чем перейти к практике, нужно обсудить несколько вопросов.


Что же мы хотим получить?


Самый главный вопрос, на который нужно дать ответ это "Какой результат мы хотим получить?". Да, в прошлый раз мы много говорили об автоматизации установки, развертывания и настройки виртуалок, но в отрыве от конечной цели это всё не имеет большого смысла.


Лично для меня системные тесты "всё в одном" выглядят так: я скачиваю из VCS несколько небольших файликов (сам скрипт с запуском, плюс, возможно, несколько вспомогательных артефактов), подкладываю куда нужно тестируемую программу (в виде инсталлятора или пакета, например), нажимаю одну кнопку и иду пить кофе. Когда я возвращаюсь, я хочу либо увидеть что все тесты прошли, либо что такие-то тесты сломались. Я не хочу заниматься какой-либо настройкой стенда, не хочу разворачивать виртуалки или что-то там настраивать. Хочу скачать скрипт и воспользоваться им.


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


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


Что будем тестировать


Для статьи в качестве подопытной программы я выбрал интересную кандидатуру. Мы будем тестировать мини-фаервол, написанный с ипользованием библиотеки Data Plane Development Kit (DPDK). Если вы не знакомы с DPDK не пугайтесь, мы не собираемся его изучать, мы возьмём готовое приложение из тех примеров, которые поставляются вместе с DPDK. Приложение на DPDK идеально подходит к этой статье, потому что совершенно непонятно, как именно можно автоматизировать end-to-end тесты для таких приложений.


Несколько слов про DPDK и что в нем такого особенного

DPDK (Data Plane Development Kit) под этим громким названием скрывается просто набор библиотек, написанных на языке C. Эти библиотеки упрощают создание приложений, которые занимаются обработкой сетевого трафика. Примечательной особенностью этой библиотеки является тот факт, что она взаимодействует с сетевыми адаптерами напрямую. Обычно оборудованием управляет операционная система, она же выполняет приём и обработку сетевых пакетов. Платформа DPDK позволяет отодвинуть операционную систему в сторону и взять всё управление в свои руки. Зачем это нужно? Такой подход позволяет добиться впечатляющих показателей производительности. Дело в том, что ядро операционной системы, например Linux, выполняет с сетевыми пакетами очень много манипуляций, которые нам, возможно, и не нужны. Оно и понятно, ведь Linux он как швейцарский нож, подходит для решения практически любой задачи. Если же нужно решить всего одну относительно простую задачу и сделать это максимально эффективно, то DPDK будет хорошим выбором.


Само приложение имеет очень простой принцип работы:


  1. оно принимает сетевые пакеты с одного из сетевых интерфейсов;
  2. сопоставляет полученный пакет со списком правил фильтрации;
  3. если пакет попал под правило DROP то пакет отбрасывается;
  4. если пакет попал под правило ACCEPT то пакет выплёвывается из другого сетевого адаптера;

Соответственно, нам нужно проверить, что:


  • приложение устанавливается на ОС и успешно запускается;
  • правила фильтрации отрабатывают так, как и было задумано;

План работ


Как и в предыдущей части, давайте для начала представим, какие действия нам нужно было бы проделать вручную, если бы мы хотели протестировать такой базовый фаерволл с использованием виртуалок:


  1. Создать три виртуальных машины (client, middlebox, server), установить везде Ubuntu Server 18.04 (например);
  2. Создать две виртуальные сети: сеть между client и middlebox (назовём эту сеть для краткости net_1) и сеть между middlebox и server (назовём её net_2);
  3. Подключить машины к этим сетям;
  4. Настроить виртуальные машины, привести их в боевое состояние;
  5. Установить наше приложение-фаервол в машину middlebox;
  6. Прогнать сами тесты.

Вот все эти вещи мы и хотим автоматизировать в нашем скрипте. При этом добавим пару оговорок:


  1. Как мы помним из первой части статьи, для выполнения команд на виртуалках нам потребуется организовать SSH-канал управления между виртуалками и хостом, это будет один из пунктов, который нам нужно будет дополнительно сделать, хотя для ручного тестирования этот пункт явно необязателен;
  2. Хоть теоретически хост может связаться с виртуалкой по SSH используя виртуальные сети net_1 и net_2, но лучше использовать для этого отдельную сеть (назовём её net_for_ssh). Глобальных причин для этого две:

  • Т.к. мы тестируем фаерволл, то не хотелось бы, чтобы управляющий трафик оказывал какое-либо влияние на ход проведения тестов;
  • В случае, если что-то пойдет не так (например, фаерволл свалится), мы бы не хотели чтобы управление у виртуалки отваливалось.

За работу!


Для тестирования мини-фаерволла мы развернём такой стенд:




С учётом знаний, которые мы получили в предыдущей части статьи, автоматизировать развёртывание стенда совсем несложно:


run_tests.sh
#!/bin/bashset -euo pipefail# =======================================# Подготовка сети net_for_ssh# =======================================virsh net-define net_for_ssh.xmlvirsh net-start net_for_ssh# =======================================# Подготовка сети net_1# =======================================virsh net-define net_1.xmlvirsh net-start net_1# =======================================# Подготовка сети net_2# =======================================virsh net-define net_2.xmlvirsh net-start net_2# =======================================# Подготовка машины client# =======================================virt-builder ubuntu-18.04 \    --format qcow2 \    --output client.qcow2 \    --install wget \    --root-password password:1111 \    --run-command "ssh-keygen -A" \    --run-command "sed -i \"s/.*PermitRootLogin.*/PermitRootLogin yes/g\" /etc/ssh/sshd_config" \    --copy-in netcfg_client.yaml:/etc/netplan/virt-install \    --import \    --name client \    --ram 1024 \    --disk client.qcow2 \    --network network=net_for_ssh \    --network network=net_1,mac=52:54:56:11:00:00 \    --noautoconsole# =======================================# Подготовка машины middlebox# =======================================virt-builder ubuntu-18.04 \    --format qcow2 \    --output middlebox.qcow2 \    --install python,daemon,libnuma1 \    --root-password password:1111 \    --run-command "ssh-keygen -A" \    --run-command "sed -i \"s/.*PermitRootLogin.*/PermitRootLogin yes/g\" /etc/ssh/sshd_config" \    --copy-in netcfg_middlebox.yaml:/etc/netplan/virt-install \    --import \    --name middlebox \    --vcpus=2,sockets=1,cores=2,threads=1 \    --cpu host \    --ram 2048 \    --disk middlebox.qcow2 \    --network network=net_for_ssh \    --network network=net_1,model=e1000 \    --network network=net_2,model=e1000 \    --noautoconsole# =======================================# Подготовка машины server# =======================================virt-builder ubuntu-18.04 \    --format qcow2 \    --output server.qcow2 \    --install nginx \    --root-password password:1111 \    --run-command "ssh-keygen -A" \    --run-command "sed -i \"s/.*PermitRootLogin.*/PermitRootLogin yes/g\" /etc/ssh/sshd_config" \    --copy-in netcfg_server.yaml:/etc/netplan/virt-install \    --import \    --name server \    --ram 1024 \    --disk server.qcow2 \    --network network=net_for_ssh \    --network network=net_2,mac=52:54:56:00:00:00 \    --noautoconsole# =======================================# Убедимся, что наши машины запустились# и доступны для команд управления# =======================================SSH_CMD="sshpass -p 1111 ssh -o StrictHostKeyChecking=no"while ! SSH_CMD root@192.168.100.2 "echo Hello world from client!" echodo    echo "Waiting for client VM ..."    sleep 1donewhile ! SSH_CMD root@192.168.100.3 "echo Hello world from middlebox!" echodo    echo "Waiting for middlebox VM ..."    sleep 1donewhile ! SSH_CMD root@192.168.100.4 "echo Hello world from server!" echodo    echo "Waiting for server VM ..."    sleep 1done

Для запуска этого скрипта потребуются следующие артефакты:


net_for_ssh.xml
<network>    <name>net_for_ssh</name>    <bridge name='net_for_ssh'/>    <ip address='192.168.100.1' netmask='255.255.255.0'/></network>

net_1.xml
<network>    <name>net_1</name>    <bridge name='net_1'/>    <ip address='192.168.101.1' netmask='255.255.255.0'/></network>

net_2.xml
<network>    <name>net_2</name>    <bridge name='net_2'/>    <ip address='192.168.102.1' netmask='255.255.255.0'/></network>

netcfg_client.yaml
network:  version: 2  renderer: networkd  ethernets:    ens3:      addresses:        - 192.168.100.2/24    ens4:      addresses:        - 192.168.101.2/24      gateway4: 192.168.101.3

netcfg_middlebox.yaml
network:  version: 2  renderer: networkd  ethernets:    ens3:      addresses:        - 192.168.100.3/24

netcfg_server.yaml
network:  version: 2  renderer: networkd  ethernets:    ens3:      addresses:        - 192.168.100.4/24    ens4:      addresses:        - 192.168.102.2/24      gateway4: 192.168.102.3

Большая часть команд и приёмов Вам должна быть уже известна из первой части статьи, а мы лишь остановимся на некоторых интересных моментах:


  1. Мы используем параметр --install команды virt-builder, чтобы установить на виртуалки дополнительные пакеты. Это просто удобное сокращение для --run-command "apt install ...". Собственно, Вам даже не обязательно знать, какой пакетный менеджер работает на гостевой системе virt-builder сам разберётся. Для машины client мы устанавливаем пакеты wget, для server nginx (чтобы тестировать фаерволл с помощью http-запросов на сервер). Для middlebox мы устанавливаем зависимости, необходимые для настройки и запуска DPDK-приложений;
  2. Для машин client и server мы указываем, какие МАС-адреса нужно присвоить сетевым адаптерам, смотрящих в сторону фаерволла. Это пригодится нам при прогоне тестов;
  3. Для машины middlebox мы указываем топологию виртуального процессора (параметр --vcpus): нам требуется один CPU c двумя ядрами без поддержки технологии hyperthreading. Два ядра это минимальное количество ядер, необходимое для запуска DPDK приложения. Кроме того мы указываем параметр --cpu host, что означает, что процессор на вируалке должен иметь те же возможности, что и процессор на хостовой системе. Дело в том, что по-умолчанию QEMU создаёт виртуальный процессор, который не поддерживает даже SSE3 инструкции. А без этого, опять же, DPDK приложение не запустится.
  4. Также для машины middlebox мы указываем модель сетевых адаптеров, участвующих в машрутизации и фильтрации трафика: e1000. Та модель адаптера, которая создаётся по-умолчанию на данный момент не поддерживается библиотекой DPDK.

На текущий момент наш скрипт run_tests.sh сможет корректно отработать только один раз (или даже вообще ни разу, если Вы проделывали у себя шаги из первой части статьи). При повторном запуске у Вас будут возникать ошибки, связанные с двумя моментами:


  1. Нельзя повторно создать уже созданную сеть;
  2. Нельзя повторно создать уже созданную виртуальную машину.

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


run_clean.sh
#!/bin/bashset -euo pipefail# =======================================# Удаление машины client# =======================================if virsh list --all | grep -q " client "; then    if virsh domstate client | grep -q "running"; then        virsh destroy client    fi    virsh undefine client --snapshots-metadata --remove-all-storagefi# =======================================# Удаление машины middlebox# =======================================if virsh list --all | grep -q " middlebox "; then    if virsh domstate middlebox | grep -q "running"; then        virsh destroy middlebox    fi    virsh undefine middlebox --snapshots-metadata --remove-all-storagefi# =======================================# Удаление машины server# =======================================if virsh list --all | grep -q " server "; then    if virsh domstate server | grep -q "running"; then        virsh destroy server    fi    virsh undefine server --snapshots-metadata --remove-all-storagefi# =======================================# Удаление сети net_for_ssh# =======================================if virsh net-list --all | grep -q " net_for_ssh "; then    if virsh net-list --all | grep " net_for_ssh " | grep -q " active "; then        virsh net-destroy net_for_ssh    fi    virsh net-undefine net_for_sshfi# =======================================# Удаление сети net_1# =======================================if virsh net-list --all | grep -q " net_1 "; then    if virsh net-list --all | grep " net_1 " | grep -q " active "; then        virsh net-destroy net_1    fi    virsh net-undefine net_1fi# =======================================# Удаление сети net_2# =======================================if virsh net-list --all | grep -q " net_2 "; then    if virsh net-list --all | grep " net_2 " | grep -q " active "; then        virsh net-destroy net_2    fi    virsh net-undefine net_2fi

Вот этот скрипт, в отличие от run_tests.sh, будет отрабатывать всегда. Основные моменты в этом скрипте:


  1. Удаляет машины или сети только если они созданы (существование виртуалок проверяется с помощью команды virsh list --all, а существование сетей с помощью команды virsh net-list -all);
  2. Чтобы удалить машину/сеть, сначала нужно убедиться, что эта машина/сеть выключена, иначе удалить её не получится;
  3. Виртуалки удаляются вместе со снепшотами (параметр --snapshots-metadata) и подключенными дисками (параметр --remove-all-storage).

Пока что для повторного запуска run_tests.sh нужно запускать run_clean.sh. В дальнейшем мы доработаем run_tests.sh так, чтобы он отрабатывал и без помощи run_clean.sh.


По поводу копипасты

Скрипт run_clean.sh явно напрашивается на рефакторинг. Можно было бы сделать цикл по именам виртуалок и сетей и таким образом избавиться от копипасты. Но я стараюсь делать скрипты максимально прямолинейными, чтобы скрипты были более читабельные, чтобы было легче разораться, как это работает.


Здесь стоит упомянуть один неприятный момент. Когда мы запускаем SSH-команды SSH добавляет публичный ключ виртуалки в файл ~/.ssh/known_hosts. Если мы удалим виртуалки, создадим её заново и попробуем поключится к ней по SSH он откажется подключаться, думая, что Вас кто-то хочет обмануть. Мы можем избежать этой ситуации подкорректировав нашу переменную SSH_CMD:


SSH_CMD="sshpass -p 1111 ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR"

Я добавил параметр -o UserKnownHostsFile=/dev/null, чтобы запуск скрипта тестового сценария никак не влиял на хостовую машину.


Немного об организации тестов


В прошлой части я упомянул, что идеальный вариант автоматизации системных тестов это автоматизация действий человека при работе за компьютером. Но работу пользователя с Ubuntu Server (без GUI) можно представить как последовательность выполнения bash-команд, чем мы и воспользовались.


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


Во-первых, сделаем очень простую, но в тоже время очень полезную вещь:


EXEC_CLIENT="$SSH_CMD root@192.168.100.2"EXEC_MIDDLEBOX="$SSH_CMD root@192.168.100.3"EXEC_SERVER="$SSH_CMD root@192.168.100.4"

Мы всего лишь объявили три переменные. Но это уже позволяет писать более читабельный скрипт. Например:


$EXEC_CLIENT echo hello from client$EXEC_SERVER echo hello from server

Во-вторых, что насчёт многострочных команд? Здесь нам поможет такая возможность bash-интерпретатора как Heredoc. Heredoc позволяет нам записывать тестовые сценарии в следующим виде:


$EXEC_CLIENT << EOF    echo hello from client    ls    pwdEOF$EXEC_MIDDLEBOX << EOF    echo hello from middlebox    some_another_commandEOF

Здесь EOF это всего лишь последовательность символов, которой мы обозначаем начало и конец строки. Вместо EOF Вы можете использовать другую последовательность символов, главное, чтобы она была одинаковой в начале и в конце строки.


Когда вы используете мультистрочные команды, скорее всего вы захотите добавить в начало строчку set -xeuo pipefail. Например:


$EXEC_MIDDLEBOX << EOF    set -xeuo pipefail    command1    command2 | command3EOF

Напомню на всяких случай, что означает эта команда:


  • параметр -x заставляет bash-интерпретатор печатать каждую команду перед тем как её выполнить;
  • параметр -e останавливает выполнение всего скрипта, если хотя бы одна из команд завершилась с ошибкой;
  • параметр -u останавливает выполнение всего скрипта, если Вы обратились в нём к несуществующей переменной;
  • парамерт -o pipeline останавливает конвейер, если какая-то команда в его составе завершилась с ошибкой.

Таким образом, если что-то пойдёт не так в Вашем тестовом сценарии Вы сразу об этом узнаете.


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


$EXEC_MIDDLEBOX << EOF    set -xeuo pipefail    command1    ! command2EOF

Вышеприведенный скрипт выполнится успешно только в одном единственном случае: если command1 вернёт 0, а command2 вернёт значение, отличное от нуля.


Переходим к самим тестам


В рамках статьи мы напишем три системных теста:


  1. Проверим, что приложение успешно устанавливается и запускается;
  2. Проверим, что приложение в принципе машрутизирует трафик;
  3. Проверим, что приложение может блокировать определенный вид трафика.

Давайте взглянем на первый тест:


$SCP_CMD l3fwd-acl-1.0.0.deb root@192.168.100.3:~$EXEC_MIDDLEBOX << EOF    set -xeuo pipefail    dpkg -i l3fwd-acl-1.0.0.deb    echo 256 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages    mkdir -p /mnt/huge    mount -t hugetlbfs nodev /mnt/huge    modprobe uio_pci_generic    dpdk-devbind --bind=uio_pci_generic ens4 ens5    echo "R0.0.0.0/0 192.168.102.0/24 0 : 65535 0 : 65535 0x0/0x0 1" > /etc/rule_ipv4.db    echo "R0.0.0.0/0 192.168.101.0/24 0 : 65535 0 : 65535 0x0/0x0 0" >> /etc/rule_ipv4.db    echo "R0:0:0:0:0:0:0:0/0 0:0:0:0:0:0:0:0/0 0 : 65535 0 : 65535 0x0/0x0 0" > /etc/rule_ipv6.db    daemon --name l3fwd --unsafe --output /var/log/l3fwd -- l3fwd-acl \        -l 1 \        -n 4 \        -- \        -p 0x3 \        -P \        --config="(0,0,1),(1,0,1)" \        --rule_ipv4="/etc/rule_ipv4.db" \        --rule_ipv6="/etc/rule_ipv6.db"EOF

О содержимом скрипта

Вот развёрнутый список того, что делает этот скрипт (для интересующихся):


  1. Устанавливает скопированный на виртуалку deb-пакет;
  2. Резервирует и монтирует 256 гигантских страниц по 2 мегабайта (DPDK-приложения по-умолчанию используют гигантские страницы для размещения своих данных);
  3. Подгружает poll-mode драйвер uio_pci_generic (он поставляется в составе Ubuntu Server). Этот драйвер необходим для того, чтобы DPDK приложение смогло через него получить прямой доступ к сетевым адаптерам;
  4. Отвязывает интерфейсы ens4 (в сторону клиента) и ens5 (в сторону сервера) от стандартного сетевого драйвера и привязывает их к драйверу uio_pci_generic;
  5. Создаёт файл rule_ipv4.db с правилами маршрутизации для пакетов IPv4 и кладёт туда два правила: все пакеты с адресом назначения 192.168.102.0/24 отправлять на порт 1 (то есть все пакеты от клиента к серверу надо отправлять на порт, который смотрит в сторону сервера), а все пакеты с адресом назначения 192.168.101.0/24 отправлять на порт 0 (то есть в сторону клиента);
  6. Также создаёт аналогичный файл для rule_ipv6.db, но туда кладёт одно правило по умолчанию "Все пакеты отправляй на порт 0". Реальных IPv6 пакетов генерироваться в рамках тестов не будет, но без этого файла DPDK приложение запускаться не будет;
  7. Запускает тестируемое l3fwd приложение и отправляет его работать в фон с помощью daemon. Подробно останавливаться на формате запуска не будем, но для интересующихся этот формат можно посмотреть на странице с документацией l3fwd: https://doc.dpdk.org/guides/sample_app_ug/l3_forward.html

Посмотрите, какие манипуляция с окружением нам приходится делать чтобы проверить приложение DPDK с точки зрения конечного пользователя:


  1. Установить пакет .deb в систему;
  2. Подправить параметры ядра;
  3. Смонтировать раздел к файловой системе;
  4. Загрузить модуль ядра;
  5. Привязать сетевые интерфейсы, участвующие в маршрутизации, к драйверу uio_pci_generic;
  6. Запустить приложение в фоне.

В этом и состоит сущность системного тестирования: мы не просто тестируем программу "в вакууме", мы не делаем никаких заглушек и ухищрений чтобы она хоть как-то заработала. Вместо этого мы помещаем программу в реальную обстановку и производим все манипуляции, которые пришлось бы сделать на реальном компьютере реальному пользователю. Это даёт колоссальный уровень спокойствия за тестируемую программу.


И этот лейтмотив только усиливается во втором тесте: вместо того, чтобы проверять работу DPDK-приложения с помощью фиксированного набора пакетов (как это можно было бы сделать в unit-тестах, например), мы будем проверять приложение так, как это бы сделал реальный человек: попробуем передать что-нибудь по сети с помощью реальных утилит и посмотрим на результат:


$EXEC_CLIENT arp -s 192.168.101.3 52:54:56:00:00:00$EXEC_SERVER arp -s 192.168.102.3 52:54:56:11:00:00$EXEC_CLIENT << EOF    set -xeuo pipefail    ping -c 5 192.168.102.2    wget --timeout=5 --tries=1 http://192.168.102.2EOF

о ситуации с ARP-записями

Перед тем, как пускать трафик, мы добавили две статические ARP-записи.
Зачем это нужно? Дело в том, что тестовое приложение l3fwd, взятое из примеров
библиотеки DPDK, настолько простое, что оно даже не обратывает протокол ARP.
Приложение l3fwd просто фильтрует трафик в соответствии с правилами, заданными
в файлах rule_ipv4.db и rule_ipv6.db, и кроме этого не делает больше ровным
счётом ничего: ни проверки чексуммы, ни фрагментации/дефрагментации пакетов, ни-че-го.
Это как раз один из способов достижения максимальной производительности: просто
не делать того, что конкретно Вам не нужно конкретно в Вашей ситуации.
Это приводит к тому, что сетевые пакеты пролетают сквозь машину middlebox
вообще без никаких изменений, хотя у него должны поменяться MAC-адреса
в Ethernet-заголовке (иначе client и server будут просто отпрасывать такие пакеты).
Мы можем закостылить эту пролему следующим образом: будем отправлять пакеты
с уже заранее изменённым destination MAC-адресом. Для этого здесь и используются
статические ARP-записи.


Третий же тест довольно очевиден и вытекает из первых двух:


# =======================================# Добавим правило, заврещающее tcp трафик# =======================================$EXEC_MIDDLEBOX << EOF    set -xeuo pipefail    daemon --name l3fwd --stop    # Это и есть запрещающее правило    echo "@0.0.0.0/0 0.0.0.0/0 0 : 65535 0 : 65535 0x06/0xff" > /etc/rule_ipv4.db    echo "R0.0.0.0/0 192.168.102.0/24 0 : 65535 0 : 65535 0x0/0x0 1" >> /etc/rule_ipv4.db    echo "R0.0.0.0/0 192.168.101.0/24 0 : 65535 0 : 65535 0x0/0x0 0" >> /etc/rule_ipv4.db    daemon --name l3fwd --unsafe --output /var/log/l3fwd -- l3fwd-acl \        -l 1 \        -n 4 \        -- \        -p 0x3 \        -P \        --config="(0,0,1),(1,0,1)" \        --rule_ipv4="/etc/rule_ipv4.db" \        --rule_ipv6="/etc/rule_ipv6.db"EOF# =======================================# Проверяем, что ping продолжает ходить,# а http трафик - перестал# =======================================$EXEC_CLIENT << EOF    set -xeuo pipefail    ping -c 5 192.168.102.2    ! wget --timeout=5 --tries=1 http://192.168.102.2EOF

Обратите внимание, что перед wget стоит восклицательный знак: это означает, что тест будет успешен, только если команда wget завершиться с ошибкой.


Дорабатываем скрипт run_tests.sh


С самими тестами мы разобрались, осталось немного подумать над нашим скриптом по запуску тестов, который сейчас может успешно отрабатывать только при предварительном прогоне скрипта run_clean.sh.


Давайте поразмышляем вот над чем: весь скрипт можно условно разделить на две больших секции: до копирования сборки DPDK-приложения на middlebox и после. В чём разница? Дело в том, что по отношению к стенду (системе из виртуальных машин) сборка DPDK-приложения является внешней переменной Х. Мы заранее знаем, как поведёт себя скрипт до появления этой внешней переменной, но мы не знаем насколько успешно пройдут тесты после внедрения внешнего неизвестного элемента. Вдруг сборка окажется неработоспособной и тесты свалятся?


Поэтому мы можем внести элемент кеширования в наш скрипт. Как насчёт фиксации результатов работы скрипта на момент ровно перед появлением внешней переменной, и отката к этому состоянию при повторном запуске скрипта? Ведь мы знаем, что участок скрипта до появления неопределённого элемента должен всегда выполняться одинаково. А фиксацию можно сделать очень просто: мы можем создать снепшот, например с названием init, для каждой виртуальной машины после её первоначальной настройки.


Осталось лишь немного подправить скрипт чтобы он учитывал эту новую логику:


# =======================================# Подготовка машины client# =======================================if ! virsh list --all | grep -q " client "then    virt-builder ubuntu-18.04 \        --format qcow2 \        --output client.qcow2 \        --hostname client \        --install wget,net-tools \        --root-password password:1111 \        --run-command "ssh-keygen -A" \        --run-command "sed -i \"s/.*PermitRootLogin.*/PermitRootLogin yes/g\" /etc/ssh/sshd_config" \        --copy-in netcfg_client.yaml:/etc/netplan/    virt-install \        --import \        --name client \        --ram 1024 \        --disk client.qcow2 \        --network network=net_for_ssh \        --network network=net_1,mac=52:54:56:11:00:00 \        --noautoconsole    virsh snapshot-create-as client --name initelse    virsh snapshot-revert client --snapshotname initfi

Теперь если машина уже существует, то скрипт вместо её создания будет откатывать её к снепшоту init.


Другие снепшоты

Конечно, никто не запрещает создавать и другие снепшоты уже во время самих тестов, чтобы при необходимости откатываться к ним: например, мы могли бы создать снепшот после установки .deb-пакета и базовых настроек DPDK, после чего откатываться к нему перед началом второго теста. Здесь лучше всего действовать по ситуации.


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


# =======================================# Подготовка сети net_1# =======================================if ! virsh net-list --all | grep -q " net_1 "then    virsh net-define net_1.xml    virsh net-start net_1fi

Вот и всё, теперь наш скрипт мало того, что запускается каждый раз без особых проблем, так ещё и экономит нам значительное количество времени на повторных запусках за счёт кеширования процесса подготовки стенда. Ну а Вас остаётся только поздравить: ведь теперь Вы умеете создавать настоящие системные тесты с использованием виртуалок!


Репо с итоговым скриптом можно найти здесь: https://github.com/testo-lang/testo-articles/tree/master/01


Итоги


Что ж, мы проделали довольно много работы, в том числе и муторной, и самое время оглянуться назад и посмотреть, чего же мы достигли. А достигли мы немалого: мы написали скрипт, который в автоматическом режиме полностью с нуля разворачивает у нас на компьютере стенд из трёх виртуальных машин, раскатывает на этом стенде сборку тестируемого приложения, а также прогоняет настоящие системные (end-to-end) тесты для этого приложения.


Заметьте, что эти тесты проверяют работу нашего приложения от начала и до конца именно в том виде, в котором это приложение будет в реальности работать: мы поместили DPDK-приложение в конкретную среду (стенд из трех машин Ubuntu Server 18.04) и с конкретным набором реальных пользовательских проверок (вызов утилиты ping и wget). В нашем стенде нет ни единой заглушки, мы можем взять наш .deb пакет и прямо сейчас выложить его на сайте для скачивания всеми желающими. И именно поэтому такие тесты дают такую четкую уверенность, что если программа работает внутри тестов, то она точно отработает в реальных условиях (по крайней мере, в ТАКИХ ЖЕ реальных условиях). И эта уверенность дорогого стоит.


Но это ещё не всё: все наши артефакты это два скрипта (run_tests.sh и run_clean.sh), три xml-файла и три yaml-файла. Всё это текстовые файлы и идеально хранятся в любой VCS. Эти скрипты можно легко переносить между компьютерами и всё равно прогонять тесты одной кнопкой.


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


Ну а если у Вас осталось некоторое ощущение "недоделанности", "костыльности" и "кривизны" подхода, который мы продемонстрировали в этой статье, то Вы, на самом деле, будете правы. Все эти наработки очень даже хороши как Proof Of Concept для идеи автоматизации системных тестов с использованием виртуальных машин. Но строить полноценную большую и промышленную систему тестов на основе баш-скриптов было бы довольно оптимистично. Впрочем, есть и более элегентное, законченное и отработаное решение, которое может помочь Вам клепать системные тесты как горячие пирожки на любых операционных системах и даже не задумываться всерьез обо всей сложной внутренней кухне, которая при всём при этом происходит. Но это уже тема для нашей следующей статьи...

Подробнее..

Топ-11 лучших систем управления тестированием 2020

07.10.2020 20:16:52 | Автор: admin
Каждый проект уникален и у каждой команды свои запросы. Однако всех нас объединяет желание работать с качественными инструментами, которые экономят время и позволяют QA-специалистам тестировать качественнее и быстрее, в идеале чтобы TMS могла в автотесты.

Мы вновь проанализировали проверенные временем и новые системы управления тестированием, которые сейчас популярны на рынке. Выбрали функции, которые должны быть в Test Management System нашей мечты, сравнили возможности продуктов и изучили отзывы пользователей. Делимся списком инструментов, один из которых точно подойдёт вашей команде.

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

image

Что мы хотим от удобной Test Management System?


Пользователь TMS ожидает увидеть следующее:

  • Удобная установка и поддержка.
  • Удобный и понятный интерфейс.
  • Создание и управление проектами.
  • Создание пользователей и проектных ролей для пользователей.
  • Удобная интеграция с автоматическими тестами.
  • Создание тест-плана.
  • Создание тест-кейса.
  • Создание чек-листа.
  • Создание общего шага
  • Версионирование тест-кейса/чек-листа.
  • Создание пользовательских атрибутов/конфигураций.
  • Прогон тест-кейса/чек-листа.
  • Понятная система отчётности.
  • Встроенная система баг-трекинга.
  • Возможность оповещения коллег внутри и вне системы.
  • Возможность интеграции с другими инструментами.

Зачем нужна TMS?


Решить задачу создания единой TMS для работы со всей документацией проекта можно несколькими способами:

  1. Самый дешёвый способ не заморачиваться и выбрать Google Docs для матрицы трассируемости, а дефекты вести в open-source баг-трекере.
  2. Другой способ использовать одну из популярных TMS-ок, интегрированную с баг-трекером компании.
  3. Next-level способ выбрать Test Management System, исходя из специфики проектов, объемов задач, типов документации и используемых видов тестирования.

Очень важно подойти к вопросу выбора TMS ответственно, ведь для компании цена ошибки может оказаться высокой.

Популярные системы управления тестированием на 2020 г


  • ALM Octane
  • Test IT
  • TestRail
  • Zephyr
  • TM4J
  • Qase
  • PractiTest
  • Testuff
  • Azure
  • MTM TFS
  • Kualitee

Рассмотрим выбранные инструменты подробнее:

1. ALM Octane


ALM Octane долгожитель среди систем управления и жизненным циклом продукта, и его тестировании. Инструмент позволяет осуществлять планирование, создание, тестирование и контроль на всех этапах разработки. Сложен в первоначальном освоении, но незаменим для компании крупной руки, где особое внимание уделяется деталям производства. Поддержана функциональность общих шагов. Работа с автоматизированными тестами. Фактические время прохождения для каждого тестового сценария. Реализована функциональность вебхуков.
Присутствует внутренний баг-трекер. Удобная система конструирования отчетов.

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

image

Возможности:

  • Общий доступ к библиотекам требований и ресурсов
  • Подробные сведения о коде, тестировании, управлении рисками и их оценке, а также о соответствии требованиям
  • Быстрый доступ к показателям, например к данным о не устранённых дефектах
  • Быстрая настройка лабораторной среды для устранения ошибок конфигурации в средах Agile
  • Работа с автоматизированными тестами
  • Вебхуки
  • Удобная система отчетов
  • Создание требований и отслеживание их выполнения на всех этапах жизненного цикла приложения
  • Аналитика на любой вкус и цвет: гибко настраиваемые отчёты
  • Интеграция с 50+ инструментами

Бесплатная пробная версия: 30 дней
Ссылка на скачивание

2. Test IT


Test IT TMS, которую создают тестировщики для тестировщиков. Этот инструмент отличает продуманный и понятный интерфейс. Внутри системы можно создавать проекты и вести для каждого структурированную библиотеку тестовых случаев и чек-листов, часто повторяющиеся операции выделяются в общие шаги. Инструмент гибкий в каждом проекте создаются дополнительные пользовательские атрибуты/конфигурации, распределяются проектные роли и права, что упрощает настройку TMS под процессы компании. Test IT помогает руководителям групп тестирования равномерно распределять нагрузку между тестировщиками и контролировать исполнение работ с помощью пользовательских запросов и отчётов. Также в рамках самой системы есть возможность общаться с коллегами, не используя сторонние мессенджеры.

Разработчики приложения уделяют большое внимание автоматизированному тестированию, каждый тестовый случай в библиотеке тестов можно линковать с автотестами по API. Правильно настроенная интеграция с автотестами позволяет следить за прогонами и их результатами прямо из TMS в режиме реального времени. Вы сможете видеть, какие автоматические тесты в процессе выполнения, анализировать их результаты и просматривать исходный код прямо из Test IT. При необходимости можно создать тест-ран вне системы и заполнить его своими автотестами без линка с тестовыми сценариями.

image

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

Возможности:

  • Управление тест-планами, тест-кейсами и чек-листами
  • Общие шаги для повторяющихся действий
  • Создание пользовательских атрибутов/конфигураций
  • Автоматическое распределение тестов на QA инженеров
  • Интеграция автоматических тестов с помощью API
  • Создание тест-ранов вне системы с дальнейшим управлением состояния
  • Аналитика как по автоматическим, так и по ручным тестам
  • Внутренний чат и вебхуки во внешние системы
  • Ролевая модель и персонализация
  • Геймификация
  • Двусторонняя интеграция с JIRA
  • Расширенная функциональность API
  • Импорт из других систем управления тестированием

Бесплатная пробная версия: Открытая демо-версия на сайте
Ссылка на скачивание

3. TestRail


Это программное обеспечение удобно как для команд QA, так и для разработки. План тестирования можно выстроить как по сценарию гибкой методологии, так и для более традиционного подхода. Инструмент позволяет получить представление о ходе тестирования в реальном времени. Вы можете строить конфигурированные отчеты по необходимым вам метрикам. В новых версиях появилась интеграция с Assembla. Так же была поддержана функциональность внутреннего чата и оповещений во внешнюю систему.

image

Помимо возможности разложения своих тестовых сценариев по тест-ранам с их дальнейшим помещением в тест-планы, вы можете использовать такую сущность как Milestone, которая позволит удобно настроить процесс прохождения тестирования.

Можно настроить типизации проекта для ведения в нем тестовой документации.

Возможности:

  • Отслеживание состояния и результатов отдельного теста
  • Сравнение результатов нескольких тестов, конфигураций и этапов
  • Типизация проектов для ведения тестовой документации
  • Внутренний чат и оповещения во внешнюю систему
  • Отслеживание рабочей нагрузки команды для корректировки задач и ресурсов
  • Развёрнутые отчёты и метрики
  • Широкие возможности настройки, облачные или локальные варианты установки
  • Интеграция с JIRA, Redmine, YouTrack, GitHub, Jenkins, Selenium и Visual Studio
  • Удобный REST API

Бесплатная пробная версия: 30 дней
Ссылка на скачивание

4. Zephyr


Zephyr это плагин для всем известной JIRA, который интегрирует тестирование в проектный цикл, позволяя вам отслеживать качество программного обеспечения и принимать решения в стиле go / no-go. В новых версиях была поддержана работа с автоматизированными тестами.

image

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

Возможности:

  • Ссылка на user-stories, задачи, требования, дефекты
  • Конфигурации деплоя: в облаке, на сервере, в дата-центре
  • Расширенная информация на дашбордах аналитики и DevOps
  • Интеграция с JIRA, Confluence, Selenium, Jenkins и Bamboo
  • Автоматизированные тесты
  • Создание пользовательских атрибутов
  • Понятная система отчетов.

Бесплатная пробная версия: 30 дней
Ссылка на скачивание

5. TM4J


TM4J он же Adaptavist это приложение для управления тестированием в JIRA, позволяет вести тестовую документацию в JIRA. Линкование тестовых сценариев и issue непосредственно в JIRA. Поддержана работа с автоматизированными тестами. Возможность объединения повторяющихся шагов в общий шаг.

image

Возможности:

  • Линкование тестовых сценариев и issue не выходя из JIRA
  • Работа с автоматизированными тестами
  • Внутренний багтрекер
  • Понятная система отчетов
  • Использование общего шага
  • Фактическое время прохождения теста
  • Экспорт данных в Excel

Бесплатная пробная версия: 30 дней
Ссылка на скачивание

6. Qase


Qase это облачная TMS, которая поможет вашей команде повысить производительность и организовать удобный флоу тестирования программного обеспечения. Поддержана функциональность объединения постоянно повторяющихся действий в общий шаг, импорт данных из других TMS. Управление ролями и разрешениями для пользователей системы. В новой версии была доработана система отчетов.

image

Возможности:

  • Тестовый репозиторий: выстраивание тестов в логические группы
  • Составление шагов для кейсов, установка приоритета и серьёзности
  • Запуск тестовых прогонов с трекингом времени по каждому тест
  • Хранение документации по проекту
  • Автоматическое заведение дефектов в интегрированные трекеры
  • Интеграция с JIRA, Redmine, YouTrack и Slack
  • Объединение результатов автотестов с REST API

Бесплатно для небольших компаний
Ссылка на скачивание

7. PractiTest


PractiTest это комплексное средство управления тестами. Оно дает полную картину процесса тестирования и более глубокое понимание результатов тестирования. Этот инструмент поможет организовать тест-сьюты в соответствии с вашими циклами и спринтами. Тестовые наборы можно формировать по различным критериям, таким как компоненты, версии или типы. Тул заточен на Agile-тестирование, регрессионное тестирование, тестирование микросервисов и DevOps.

image

В новых версиях была доработана функциональность работы с автоматизированными тестами.

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

Возможности:

  • Лёгкое добавление тестов новых фич в регрессионное тестирование
  • Группировка тестов на основе микросервисов, которые они охватывают, даже кросс-сервисные
  • Различное отображение информации для разных групп пользователей
  • Дашборды в реальном времени показывают состояние тестов, прогонов на этапах разработки и при деплое на прод
  • Интеграция с JIRA, Redmine, Jenkins, GitLab и Slack

Бесплатная пробная версия: 14 дней
Ссылка на скачивание

8. Testuff


Команда Testuff делает действительно удобный инструмент, данная TMS старается объединить в себе все методы тестирования, начиная от waterfall model и заканчивая black box testing.
Разработчики Testuff отдельно выделили свой продукт как единственную TMS, которую можно использовать на любом девайсе: смартфоны, планшеты и т.д

image

В данном решении есть все необходимое для TMS: классный интерфейс, понятный и удобный способ ведения тестовой документации, конфигурации и пользовательские атрибуты, два способа интеграции с любым существующим баг-трекером, а также добавили видеорекордер.

Возможности:

  • Планирование тестов
  • Интуитивный drag-n-drop интерфейс
  • Наглядные отчёты с подробными графиками
  • Два способа интеграции со сторонними инструментами баг-трекинга
  • Возможность тестировать с любого девайса

Бесплатная пробная версия: 30 дней
Ссылка на скачивание

9. Azure DevOps Server


Это мощный инструмент работы с тестами и автотестами, за счет своей комплексности вы можете настроить своё рабочее пространство, как вам необходимо. Работайте напрямую со своими CI/CD сервисами, интегрируйте свои репозитории прямиком в Azure, ведите тестовую документацию по спринтам, которые будете раскладывать по бордам, делайте максимально детальные отчеты по вашей тестовой документации и результатам её прохождения.

image

Отдельно стоит упомянуть возможность интеграции с IDE от компании Microsoft, вы можете редактировать и настраивать свой код прямиком через Azure и интегрироваться со всевозможными системами от компании Microsoft.

Возможности:

  • Интеграция с любым продуктом компании Microsoft
  • Нативный интерфейс
  • Интеграция с любым CI/CD
  • Ведение удобных Dashboards
  • Работа с автотестами
  • Пользовательские атрибуты

Бесплатная пробная версия: 30 дней
Ссылка на скачивание

10. MTM TFS


Team Foundation Server (TFS) комплексное решение от Microsoft, которое включает в себя систему управления версиями, сбор данных, построение отчетов, отслеживание статусов и изменений по проекту.

Microsoft Test Manager часть этого продукта и требует установки Visual Studio. Такое сочетание дает возможность связать задачи, которые поставлены перед тестировщиком, с заведенными дефектами и отчетами о затраченном на работу времени.

image

Планы и результаты тестирования сохраняются на сервере Team Foundation Server.
МТМ включает в себя тест-план, тест-кейс и конфигурации.

Сам TFS является проприетарным ПО, лицензия коммерческая. Работает на трех уровнях: клиентский уровень, прикладной уровень и уровень данных, в зависимости от чего возможна работа или через web, или через десктоп-приложение. МТМ работает только на прикладном уровне, поэтому требуется установка на сервер (если сервер удаленный, работа проводится через VPN).

Возможности:

  • Исследовательское тестирование
  • Планирование и выполнение ручных тестов
  • Кроссплатформенные конфигурации тестов (разные версии одного теста для разных платформ/релизов)
  • Диагностика прохождения теста (логи, видео и т. п.)
  • Импорт-экспорт тестов
  • Межпроектный импорт-экспорт тестов
  • Запись и воспроизведение ручных тестов (рекордер)
  • Автоматизация тестов

Бесплатная пробная версия: 30 дней
Ссылка на скачивание

11. Kualitee


Kualitee продукт Kualitatem, компании по тестированию программного обеспечения и информационной безопасности, специализирующейся на обеспечении безошибочной работы приложений повсюду.

image

Kualitee предлагает функции управления проектами, управления тестированием, управления дефектами с интеграцией с баг-трекерами. Гибкая система пользовательских атрибутов позволяет очень точно настроить необходимое рабочее пространство. Дополнительно есть возможность глубокой настройки профилей пользователя и его прав доступа.

Возможности:

  • Управление проектами
  • Управление дефектами
  • Управление тестовой документацией
  • Персональная панель инструментов
  • Продуманная настройка пользователей

Бесплатная пробная версия: 30 дней
Ссылка на скачивание

Понравился пост? Не забудьте поделиться им!
И помните, только тестировщик стоит между багами и клиентом! :)
Подробнее..

Перевод Flame-графики огонь из всех движков

12.10.2020 18:16:09 | Автор: admin

Всем снова привет! Приглашаем на онлайн-встречу с Василием Кудрявцевым (директором департамента обеспечения качества в АО РТЛабс), которая пройдёт в рамках курса Нагрузочное тестирование. И публикуем перевод статьи Michael Hunger software developer and editor ofNeo4j Developer BlogandGRANDstack!

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

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

Flame-график сортирует и агрегирует трассировки на каждом уровне стека, таким образом их количество отражает процент от общего времени, затраченного в этой части кода. Визуализация этих блоков в виде прямоугольников с шириной, пропорциональной проценту от общего времени работы и укладка их друг на друга оказалась крайне полезной визуализацией. (Обратите внимание на то, что порядок слева направо не имеет принципиального значения, часто это просто сортировка по алфавиту. То же самое и с цветами. Значение имеют только относительная ширина и глубина стека)

Flame-график бенчмарка заполнения непредаллоцированного ArrayListFlame-график бенчмарка заполнения непредаллоцированного ArrayList

Flame'ы снизу вверх отражают прогрессию от точки входа программы или потока (main или цикл событий) до листьев выполнения на концах flame'ов.

Вы сразу увидите, если некоторые части программы будут занимать неожиданно большое количество времени. Чем выше на диаграмме вы это видите, тем хуже. А если у вас есть очень широкий flame сверху, то вы точно нашли узкое место, где работа не делегируется куда-либо еще. После устранения проблемы, измерьте время работы еще раз, если проблема с производительностью сохранится, вернитесь к диаграмме, чтобы понять, где осталась проблема.

Для устранения недостатков стандартных профайлеров многие современные инструменты используют внутреннюю функцию JVM (AsyncGetCallTrace), которая позволяет собирать трассировки стека независимо от безопасных точек. Помимо этого, они объединяют измерение JVM-операций с нативным кодом и системных вызовов операционной системы, так что время, проведенное в сети, ввод/вывод или сборка мусора, также может стать частью flame-графа.

Такие инструменты, как Honest Profiler, perf-map-agent, async-profiler или даже IntelliJ IDEA, умеют захватывать информацию и с легкостью делать flame-графики.

В большинстве случаев вы просто скачиваете инструмент, предоставляете PID вашего Java-процесса и говорите инструменту работать в течение определенного периода времени и генерировать интерактивный SVG.

# download and unzip async profiler for your OS from:# https://github.com/jvm-profiling-tools/async-profiler./profiler.sh -d <duration> -f flamegraph.svg -s -o svg <pid> && \open flamegraph.svg  -a "Google Chrome"

SVG, создаваемый этими инструментами, не просто красочный, но и интерактивный. Вы можете увеличивать секции, искать посимвольно и т.д.

Flame-графики впечатляющий и мощный инструмент для быстрого обзора характеристик производительности ваших программ. С ними вы можете сразу увидеть проблемные места и сосредоточиться на них, а обзор аспектов, не связанных с JVM, помогает получить более полную картину.


Интересно развиваться в данном направлении? Запишитесь на бесплатный Demo-урок Скрипты и сценарии нагрузочного тестирования- Performance center (PC) и Vugen!

Подробнее..

Пожалуй, лучшая архитектура для UI тестов

16.10.2020 18:23:47 | Автор: admin

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


Привет, Хабр! Меня зовут Диана, я руководитель группы тестирования пользовательских интерфейсов, автоматизирую веб и десктоп тесты уже пять лет. Примеры кода будут на java и для web, но, на практике проверено, подходы применимы и к питону с десктопом.

В начале было...


Вначале было слово, и слов было много, и заполняли они все страницы равномерно кодом, не обращая внимания на эти ваши архитектуры и принципы DRY (dont repeat yourself не надо повторять код, который вы уже написали три абзаца выше).

Простыня


На самом деле, архитектура портянки, она же простыня, она же сваленный в кучу неструктурированный код, равномерно заполняющий экран, не так уж плоха и вполне применима в следующих ситуациях:

  • Быстроклик в три строчки (ну ладно, в двести три) для очень маленьких проектов;
  • Для примеров кода в мини-демо;
  • Для первого кода в стиле хелоу ворд среди автотестов.

Что нужно сделать для получения архитектуры Простыни? просто записать весь нужный код в один файл, общим полотном.

import com.codeborne.selenide.Condition;import com.codeborne.selenide.WebDriverRunner;import org.testng.annotations.Test;import static com.codeborne.selenide.Selenide.*;public class RandomSheetTests {    @Test    void addUser() {        open("https://ui-app-for-autotest.herokuapp.com/");        $("#loginEmail").sendKeys("test@protei.ru");        $("#loginPassword").sendKeys("test");        $("#authButton").click();        $("#menuMain").shouldBe(Condition.appear);        $("#menuUsersOpener").hover();        $("#menuUserAdd").click();        $("#dataEmail").sendKeys("mail@mail.ru");        $("#dataPassword").sendKeys("testPassword");        $("#dataName").sendKeys("testUser");        $("#dataGender").selectOptionContainingText("Женский");        $("#dataSelect12").click();        $("#dataSelect21").click();        $("#dataSelect22").click();        $("#dataSend").click();        $(".uk-modal-body").shouldHave(Condition.text("Данные добавлены."));        WebDriverRunner.closeWebDriver();    }}


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

PageObject


Слышали слухи, что PageObject устарел? Вы просто не умеете его готовить!

Основной рабочей единицей в этом паттерне является страница, то есть законченный набор элементов и действий с ними, например, MenuPage класс, описывающий все действия с меню, то есть клики по вкладкам, раскрытие выпадающих подпунктов и так далее.



Чуть сложнее составить PageObject для модального окошка (для краткости модалки) создания объекта. Набор полей класса понятен: все поля ввода, чекбоксы, выпадающие списки; а для методов есть два варианта действий: можно сделать как универсальные методы заполни все поля модалки, заполни все поля модалки рандомными значениями, проверь все поля модалки, так и отдельные методы заполни название, проверь название, заполни описание и так далее. Что использовать в конкретном случае определяется приоритетами подход один метод на всю модалку увеличивает скорость написания теста, но по сравнению с подходом по одному методу на каждое поле сильно проигрывает в читаемости теста.

Пример
Составим общий Page Object создания пользователей для обоих видов тестов:
public class UsersPage {    @FindBy(how = How.ID, using = "dataEmail")    private SelenideElement email;    @FindBy(how = How.ID, using = "dataPassword")    private SelenideElement password;    @FindBy(how = How.ID, using = "dataName")    private SelenideElement name;    @FindBy(how = How.ID, using = "dataGender")    private SelenideElement gender;    @FindBy(how = How.ID, using = "dataSelect11")    private SelenideElement var11;    @FindBy(how = How.ID, using = "dataSelect12")    private SelenideElement var12;    @FindBy(how = How.ID, using = "dataSelect21")    private SelenideElement var21;    @FindBy(how = How.ID, using = "dataSelect22")    private SelenideElement var22;    @FindBy(how = How.ID, using = "dataSelect23")    private SelenideElement var23;    @FindBy(how = How.ID, using = "dataSend")    private SelenideElement save;    @Step("Complex add user")    public UsersPage complexAddUser(String userMail, String userPassword, String userName, String userGender,                                     boolean v11, boolean v12, boolean v21, boolean v22, boolean v23) {        email.sendKeys(userMail);        password.sendKeys(userPassword);        name.sendKeys(userName);        gender.selectOption(userGender);        set(var11, v11);        set(var12, v12);        set(var21, v21);        set(var22, v22);        set(var23, v23);        save.click();        return this;    }    @Step("Fill user Email")    public UsersPage sendKeysEmail(String text) {...}    @Step("Fill user Password")    public UsersPage sendKeysPassword(String text) {...}    @Step("Fill user Name")    public UsersPage sendKeysName(String text) {...}    @Step("Select user Gender")    public UsersPage selectGender(String text) {...}    @Step("Select user variant 1.1")    public UsersPage selectVar11(boolean flag) {...}    @Step("Select user variant 1.2")    public UsersPage selectVar12(boolean flag) {...}    @Step("Select user variant 2.1")    public UsersPage selectVar21(boolean flag) {...}    @Step("Select user variant 2.2")    public UsersPage selectVar22(boolean flag) {...}    @Step("Select user variant 2.3")    public UsersPage selectVar23(boolean flag) {...}    @Step("Click save")    public UsersPage clickSave() {...}    private void set(SelenideElement checkbox, boolean flag) {        if (flag) {            if (!checkbox.isSelected()) checkbox.click();        } else {            if (checkbox.isSelected()) checkbox.click();        }    }}

А в классе тестов пользователей распишем тест с комплексными действиями:
    @Test    void addUser() {        baseRouter.authPage()                .complexLogin("test@protei.ru", "test")                .complexOpenAddUser()                .complexAddUser("mail@test.ru", "pswrd", "TESTNAME", "Женский", true, false, true, true, true)                .checkAndCloseSuccessfulAlert();    }

И с подробными действиями:
    @Test    void addUserWithoutComplex() {        //Arrange        baseRouter.authPage()                .complexLogin("test@protei.ru", "test");        //Act        baseRouter.mainPage()                .hoverUsersOpener()                .clickAddUserMenu();        baseRouter.usersPage()                .sendKeysEmail("mail@test.ru")                .sendKeysPassword("pswrd")                .sendKeysName("TESTNAME")                .selectGender("Женский")                .selectVar11(true)                .selectVar12(false)                .selectVar21(true)                .selectVar22(true)                .selectVar23(true)                .clickSave();        //Assert        baseRouter.usersPage()                .checkTextSavePopup("Данные добавлены.")                .closeSavePopup();    }

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


Суть в том, что внутри пейджей инкапсулируются (реализация скрыта, доступны только логические действия) все действия со страницами, таким образом, в тесте используются уже бизнес-функции. А это в свою очередь позволяет писать свои пейджи под каждую платформу (веб, десктоп, мобилки), не меняя при этом тесты.

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

Для уменьшения расхождения между интерфейсами появляется соблазн усложнения отдельных шагов, их выносят в отдельные промежуточные классы, а тесты становятся все менее читаемыми, вплоть до двух шагов: авторизоваться, сделать хорошо, тест закончен. Кроме веба в наших проектах дополнительных интерфейсов не было, а читать кейсы приходится чаще, чем писать, поэтому в угоду читаемости исторические PageObject приобрели новый вид.

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

А что еще бывает?


Как ни странно, не PageObjectом единым!

  • Часто встречается паттерн ScreenPlay, о котором можно почитать например тут. У нас он не прижился, так как использовать bdd-подходы без вовлечения людей, не умеющих читать код бессмысленное насилие над автоматизаторами.
  • У js-фреймворков появляются свои собственные упрощающие жизнь подходы, помимо обязательного PageObject, но при их разнообразии говорить о чем-то устоявшемся и универсальном, мне кажется, слишком смело.
  • Можно написать и что-то свое, например, фреймворк на основе ModelBaseTesting, о чем хорошо рассказали в докладе с гейзенбага докладе с гейзенбага. Этот подход используется в первую очередь в проектах со сложносвязанными объектами, когда обычных тестов не хватает для проверки всех возможных комбинаций состояний и взаимодействий объектов.

А я вам расскажу подробнее про Page Element, позволяющий уменьшить количество однотипного кода, повысив при этом читаемость и обеспечив быстрое понимание тестов даже у тех, кто не знаком с проектом. А еще на нем (со своими блекджеками и преферансами, конечно!) построены популярные не-js фреймворки htmlElements, Atlas и епамовский JDI.

Что такое Page Element?



Для построения паттерна Page Element начнем с самого низкоуровневого элемента. Как говорит Викисловарь, виджет программный примитив графического интерфейса пользователя, имеющий стандартный внешний вид и выполняющий стандартные действия. Например, самый простой виджет Кнопка на него можно кликнуть, у него можно проверить текст и цвет. В Поле ввода можно ввести текст, проверить, какой текст введен, кликнуть, проверить отображение фокуса, проверить количество введенных символов, ввести текст и нажать Enter, проверить placeholder, проверить подсветку обязательности поля и текст ошибки, и всё, что еще может понадобиться в конкретном случае. При этом все действия с этим полем стандартны на любой странице.



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

  • Клик по элементу оглавления с заданным текстом,
  • Проверка существования элемента с заданным текстом,
  • Проверка отступа элемента с заданным текстом.


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

Для уменьшения всеобщего хаоса виджеты, как элементы страницы, объединяются во все те же пейджи, откуда, видимо, и составлено название Page Element.

public class UsersPage {    public Table usersTable = new Table();    public InputLine email = new InputLine(By.id("dataEmail"));    public InputLine password = new InputLine(By.id("dataPassword"));    public InputLine name = new InputLine(By.id("dataName"));    public DropdownList gender = new DropdownList(By.id("dataGender"));    public Checkbox var11 = new Checkbox(By.id("dataSelect11"));    public Checkbox var12 = new Checkbox(By.id("dataSelect12"));    public Checkbox var21 = new Checkbox(By.id("dataSelect21"));    public Checkbox var22 = new Checkbox(By.id("dataSelect22"));    public Checkbox var23 = new Checkbox(By.id("dataSelect23"));    public Button save = new Button(By.id("dataSend"));    public ErrorPopup errorPopup = new ErrorPopup();    public ModalPopup savePopup = new ModalPopup();}


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

    @Test    public void authAsAdmin() {        baseRouter                .authPage().email.fill("test@protei.ru")                .authPage().password.fill("test")                .authPage().enter.click()                .mainPage().logoutButton.shouldExist();    }


Можно добавить классический слой степов, если есть необходимость для этого в вашем фреймворке (реализация remote библиотеки на Java для RobotFramework требует на вход класс степов, например), или если хочется добавить аннотаций для красивых отчетов. Мы это сделали генератором на основе аннотаций, если интересно, пишите в комментах, расскажем.

Пример класса степов авторизации
public class AuthSteps{    private BaseRouter baseRouter = new BaseRouter();    @Step("Sigh in as {mail}")    public BaseSteps login(String mail, String password) {        baseRouter                .authPage().email.fill(mail)                .authPage().password.fill(password)                .authPage().enter.click()                .mainPage().logoutButton.shouldExist();        return this;    }    @Step("Fill E-mail")    public AuthSteps fillEmail(String email) {        baseRouter.authPage().email.fill(email);        return this;    }    @Step("Fill password")    public AuthSteps fillPassword(String password) {        baseRouter.authPage().password.fill(password);        return this;    }    @Step("Click enter")    public AuthSteps clickEnter() {        baseRouter.authPage().enter.click();        return this;    }    @Step("Enter should exist")    public AuthSteps shouldExistEnter() {        baseRouter.authPage().enter.shouldExist();        return this;    }    @Step("Logout")    public AuthSteps logout() {        baseRouter.mainPage().logoutButton.click()                .authPage().enter.shouldExist();        return this;    }}public class BaseRouter {// Класс для создания страниц, чтобы не дублировать этот код везде, где понадобится обращение к странице    public AuthPage authPage() {return page(AuthPage.class);}    public MainPage mainPage() {return page(MainPage.class);}    public UsersPage usersPage() {return page(UsersPage.class);}    public VariantsPage variantsPage() {return page(VariantsPage.class);}}



Эти степы очень похожи на степы внутри пейджей, практически ничем не отличаются. Но выделение их в отдельные классы открывает простор для генерации кода, при этом, жесткая связь с соответствующей страницей не теряется. При этом, если в пейдже не прописывать степы, то смысл инкапсуляции исчезает, а если не добавлять класс степов в pageElement, то взаимодействие со страницей всё равно остается отделенным от бизнес-логики.

Таким образом, для составления теста нужно описать простые действия с каждым элементом страницы, причем некоторые элементы вроде полей ввода универсальны и могут быть использованы в нескольких проектах практически без изменений. После этого для каждой страницы указывается набор полей с локаторами. И все, можно писать сам тест, который гарантированно окажется читаемым и воспроизводимым, ведь он состоит из прямых указаний нажми тут, введи текст там. Проверили на десятке проектов этот подход самый читаемый, причем не надо дублировать в каждом page object однотипный код для каждого поля, ведь все нужные действия указаны один раз в виджетах!

Хранение данных


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

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

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

Для каждой сущности создается описывающая ее модель, которая в самом простом случае содержит названия и типы полей, например вот модель пользователя:

public class User {    private Integer id;    private String mail;    private String name;    private String password;    private Gender gender;    private boolean check11;    private boolean check12;    private boolean check21;    private boolean check22;    private boolean check23;    public enum Gender {        MALE,        FEMALE;        public String getVisibleText() {            switch (this) {                case MALE:                    return "Мужской";                case FEMALE:                    return "Женский";            }            return "";        }    }}


Лайфхак 1: если у вас rest-подобная архитектура клиент-серверного взаимодействия (между клиентом и сервером ходят json или xml объекты, а не кусочки нечитаемого кода), то можно загуглить json to <ваш язык> object, вероятно, нужный генератор уже есть.

Лайфхак 2: если ваши разработчики сервера пишут на том же объектно-ориентированном языке программирования, то можно использовать их модели.

Лайфхак 3: если вы джавист и компания позволяет использовать сторонние библиотеки, а вокруг нет нервных коллег, предсказывающих много боли еретикам, использующим дополнительные библиотеки вместо чистой и прекрасной Java, берите ломбок! Да, обычно IDE может сгенерировать геттеры, сеттеры, toString и билдеры. Но при сравнении наших ломбоковских моделек и разрабских без ломбока виден профит в сотни строк пустого, не несущего бизнес-логики кода на каждый класс. При использовании ломбока не надо бить по рукам тех, кто перемешивает поля и геттеры сеттеры, класс читается легче, можно получить представление об объекте сразу, без пролистывания трех экранов.

Таким образом, у нас появляются каркасы объектов, на которые нужно натянуть тестовые данные. Данные можно хранить как final static переменные, например это может быть полезно для главного админа системы, из под которого создаются другие пользователи. Использовать лучше именно final, чтобы не было соблазна данные изменять в тестах, потому что тогда следующий тест вместо админа может получить бесправного пользователя, не говоря уже про параллельный запуск тестов.

public class Users {    public static final User admin = User.builder().mail("test@protei.ru").password("test").build();}


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

    public static User getUserRandomData() {        User user = User.builder()                .mail(getRandomEmail())                .password(getShortLatinStr())                .name(getShortLatinStr())                .gender(getRandomFromEnum(User.Gender.class))                .check11(getRandomBool())                .check21(getRandomBool())                .check22(getRandomBool())                .check23(getRandomBool())                .build();//business-logic: 11 xor 12 must be selected        if (!user.isCheck11()) user.setCheck12(true);         if (user.isCheck11()) user.setCheck12(false);        return user;    }


При этом методы, творящие непосредственный рандом, лучше вынести в отдельный класс, так как они будут использоваться и в других моделях:



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

Этот способ хранения данных использует паттерн Value Object, на его основе можно добавить любых своих хотелок, в зависимости от надобностей проекта. Можно добавить сохранение объектов в базу, и таким образом подготовить систему перед тестом. Можно не рандомить пользователей, а загружать их из файлов properties (и еще одна классная библиотека ). Можно использовать везде одного и того же пользователя, но сделать так называемые Реестры данных (data registry) под каждый вид объектов, в котором к имени или другому уникальному полю объекта будет добавляться значение сквозного счетчика, и в тесте всегда будет свой уникальный testUser_135.

Можно написать свое Хранилище объектов (гуглить object pool и flyweight), из которого запрашивать необходимые сущности в начале теста. Хранилище отдает один из своих уже готовых к работе объектов и отмечает его у себя занятым. В конце теста объект возвращается в хранилище, где его по необходимости чистят, отмечают свободным и отдают следующему тесту. Так делают, если операции создания объектов очень ресурсоемкие, а при таком подходе хранилище работает независимо от тестов и может заниматься подготовкой данных под следующие кейсы.

Создание данных


Для кейсов редактирования пользователя вам определенно понадобится созданный пользователь, которого вы будете редактировать, при этом, в общем-то, тесту редактирования не важно, откуда этот пользователь взялся. Есть несколько способов его создать:

  • нажимать кнопки руками перед тестом,
  • оставить данные от предыдущего теста,
  • развернуть перед тестом из backup,
  • создать кликами по кнопкам прямо в тесте,
  • использовать API.


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

Использование результатов предыдущего теста нарушает принцип атомарности и не дает запускать тест отдельно, придется запускать всю пачку, а ui-тесты не такие уж и быстрые. Хорошим тоном считается писать тесты так, что каждый можно запустить в гордом одиночестве и без дополнительных танцев. Кроме того, бага в создании объекта, уронившая предыдущий тест, вовсе не гарантирует багу в редактировании, а в такой конструкции тест на редактирование упадет следом, и работает ли именно редактирование не узнать.

Использовать backup (сохраненный образ базы данных) с нужными для теста данными уже более менее хороший подход, особенно если backup разворачиваются автоматически или если данные в базу кладут сами тесты. Однако почему в тесте используется конкретно этот объект не очевидно, также могут начатся проблемы пересечения данных при большом количестве тестов. Иногда backup перестают корректно работать из-за обновления архитектуры базы, например, если нужно запустить тесты на старой версии, а в backup уже есть новые поля. С этим можно бороться, организовав хранилище backup под каждую версию приложения. Иногда backup перестают быть валидными опять же из-за обновления архитектуры базы регулярно появляются новые поля, поэтому backup нужно регулярно актуализировать. А еще внезапно может оказаться, что конкретно вот такой единственный пользователь-из-backup никогда не падает, а если бы пользователь был создан только что или имя ему задавали немножко рандомно, то нашли бы баг. Это называется эффект пестицида, тест перестает ловить баги, потому что приложение привыкло к одинаковым данным и не падает, а отклонений в сторону нет.

Если пользователь создан в тесте через клики по тому же самому интерфейсу, то уменьшается пестицид и уходит неочевидность появления пользователя. Минусы аналогичны использованию результатов предыдущего теста: скорость так себе, а еще если будет баг в создании, даже самый маленький (особенно баг теста, например, поменяется локатор кнопки сохранения), то мы не узнаем, работает ли редактирование.

Наконец, еще один способ создания пользователя через http-API из теста, то есть вместо кликов по кнопкам сразу отправить запрос на создание нужного пользователя. Таким образом уменьшен, насколько возможно, пестицид, очевидно, откуда взялся пользователь, а скорость создания сильно выше, чем при кликах по кнопкам. Минусы этого способа в том, что он не подходит для проектов без json или xml в протоколе обмена данными между клиентом и сервером (например если разработчики пишут используя gwt и не хотят писать дополнительный api для тестировщиков). Можно при использовании API потерять кусок логики, выполняемой админкой, и создать не валидную сущность. API может меняться, отчего тесты упадут, однако обычно об этом известно, да и изменения ради изменений никому не нужны, скорее всего это новая логика, которую все равно придется проверять. Также возможно, что и на уровне API будет бага, но от этого ни один способ кроме готовых backup не застрахован, поэтому подходы к созданию данных лучше комбинировать.

Добавим капельку API


Среди способов подготовки данных нам больше всего подошли http-API для текущих нужд отдельного теста и разворачивание backup для дополнительных тестовых данных, которые в тестах не меняются, например, иконок для объектов, чтобы тесты этих объектов не падали при багах в загрузке иконок.

Для создания объектов через API в Java оказалось удобнее всего использовать библиотеку restAssured, хоть она предназначена не совсем для этого. Хочу поделиться парой найденных фишек, знаете еще пишите!

Первая боль авторизация в системе. Её способ нужно для каждого проекта подбирать отдельно, однако есть общее авторизацию нужно выносить в спецификацию запроса, например:

public class ApiSettings {    private static String loginEndpoint="/login";    public static RequestSpecification testApi() {        RequestSpecBuilder tmp = new RequestSpecBuilder()                .setBaseUri(testConfig.getSiteUrl())                .setContentType(ContentType.JSON)                .setAccept(ContentType.JSON)                .addFilter(new BeautifulRest())                .log(LogDetail.ALL);        Map<String, String> cookies = RestAssured.given().spec(tmp.build())                .body(admin)                .post(loginEndpoint).then().statusCode(200).extract().cookies();        return tmp.addCookies(cookies).build();    }}


Можно добавить возможность сохранения Cookies конкретного пользователя, тогда количество запросов к серверу уменьшится. Второе возможное расширение этого метода сохранять полученные Cookies для текущего теста, и подкидывать их драйверу браузера, пропуская шаг авторизации. Выигрыш секунды, но если их умножить на количество тестов, можно неплохо ускориться!

Есть плюшечка для аллюра и красивых отчетов, обратите внимание на строку .addFilter(new BeautifulRest()):

Класс BeautifulRest

public class BeautifulRest extends AllureRestAssured {        public BeautifulRest() {}        public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext filterContext) {            AllureLifecycle lifecycle = Allure.getLifecycle();            lifecycle.startStep(UUID.randomUUID().toString(), (new StepResult()).setStatus(Status.PASSED).setName(String.format("%s: %s", requestSpec.getMethod(), requestSpec.getURI())));            Response response;            try {                response = super.filter(requestSpec, responseSpec, filterContext);            } finally {                lifecycle.stopStep();            }            return response;        }}



Модели объектов отлично ложатся на restAssured, так как библиотека сама справляется с сериализацией и десериализаций моделей в json/xml (превращением из json/xml форматов в объект заданного класса).

    @Step("create user")    public static User createUser(User user) {        String usersEndpoint = "/user";        return RestAssured.given().spec(ApiSettings.testApi())                .when()                .body(user)                .post(usersEndpoint)                .then().log().all()                .statusCode(200)                .body("state",containsString("OK"))                .extract().as(User.class);    }


Если рассмотреть подряд несколько степов создания объектов, можно заметить идентичность кода. Для уменьшения одинакового кода можно написать общий метод создания объектов.

    public static Object create(String endpoint, Object model) {        return RestAssured.given().spec(ApiSettings.testApi())                .when()                .body(model)                .post(endpoint)                .then().log().all()                .statusCode(200)                .body("state",containsString("OK"))                .extract().as(model.getClass());    }    @Step("create user")    public static User createUser(User user) {                  create(User.endpoint, user);    }


Еще раз про рутинные операции


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

    @Test    void checkUserVars() {        //Arrange        User userForTest = getUserRandomData();        // Проверка корректности сохранения полей уже есть в другом тесте,  // этот тест проверяет отображение вариантов из-под залогинившегося юзера,  // поэтому не важно, как юзер создан        usersSteps.createUser(userForTest);        authSteps.login(userForTest);        //Act        mainMenuSteps                .clickVariantsMenu();        //Assert        variantsSteps                .checkAllVariantsArePresent(userForTest.getVars())                .checkVariantsCount(userForTest.getVarsCount());        //Cleanup        usersSteps.deleteUser(userForTest);    }


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

    @Test    void authAsAdmin() {        authSteps.login(Users.admin);// Это всё, просто авторизовались под админом. Все действия и проверки внутри. // Не очень очевидно, не правда ли? 


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

Вместо этого нужно почитать про data-driven тесты, для Java+TestNG это будет примерно так:

    @Test(dataProvider = "usersWithDifferentVars")    void checkUserDifferentVars(User userForTest) {        //Arrange        usersSteps.createUser(userForTest);        authSteps.login(userForTest);        //Act        mainMenuSteps                .clickVariantsMenu();        //Assert        variantsSteps                .checkAllVariantsArePresent(userForTest.getVars())                .checkVariantsCount(userForTest.getVarsCount());    } // Метод возвращает пользователей с полным перебором трех булевых параметров.  // Предположим, это важное бизнес-требование.    @DataSupplier(name = "usersWithDifferentVars")    public Stream<User> usersWithDifferentVars(){        return Stream.of(            getUserRandomData().setCheck21(false).setCheck22(false).setCheck23(false),            getUserRandomData().setCheck21(true).setCheck22(false).setCheck23(false),            getUserRandomData().setCheck21(false).setCheck22(true).setCheck23(false),            getUserRandomData().setCheck21(false).setCheck22(false).setCheck23(true),            getUserRandomData().setCheck21(true).setCheck22(true).setCheck23(false),            getUserRandomData().setCheck21(true).setCheck22(false).setCheck23(true),            getUserRandomData().setCheck21(false).setCheck22(true).setCheck23(true),            getUserRandomData().setCheck21(true).setCheck22(true).setCheck23(true)        );    }


Тут используется библиотека Data Supplier, которая является надстройкой над TestNG Data Provider, позволяющей использовать типизированные коллекции вместо Object [] [], но суть та же. Таким образом мы получаем один тест, выполняемый столько раз, сколько входных данных он получает.

Выводы


Итак, для создания большого, но удобного проекта автотестов пользовательских интерфейсов нужно:
  • Описать все мелкие виджеты, встречаемые в приложении,
  • Собрать виджеты в страницы,
  • Создать модели для всех видов сущностей,
  • Добавить методы генерации всех видов сущностей на основе моделей,
  • Подумать о подходящем методе создания дополнительных сущностей
  • Опционально: сгенерировать или собрать вручную файлы степов,
  • Писать тесты так, чтобы в секции основных действий конкретного теста не было сложносоставных действий, только очевидные операции с виджетами.

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

Примеры кода из статьи в виде готового проекта добавлены в гит.
Подробнее..

Tladianta инструмент тестирования или нечто большее

10.11.2020 18:17:06 | Автор: admin


Всем привет! Я Максим Кузнецов, и я продолжаю цикл статей рассказом об инструменте автоматизированного тестирования в Росбанке.


В прошлый раз вы читали:


  1. Fast-Unit или декларативный подход к юнит-тестам
  2. Tladianta. Сервис по автоматизированному тестированию в Росбанке

Я сегодня расскажу о самом инструменте фреймворке Tladianta.


Этот инструмент задумывался как база для легкого течения процесса тестирования в его новом виде. Менеджеры придумывают процесс и управляют им, а исполнителям нужен инструмент, а лучше семейство инструментов, которое отражает и обслуживает процесс.


Tladianta это первый базовый инструмент сервиса по автоматизированному тестированию, который наша команда предоставляет на весь банк.


Какие задачи он должен решать?


1. Легкий старт и удобство настройки

Предполагается использование фреймворка во многих командах. Много-много задач типа get started. В идеале должно быть так: создал проект и начал сразу писать тестовые сценарии.


2. Легкая поддержка и возможность обновления для всех команд

Каждая команда будет использовать определенную часть фреймворка (или несколько) и эта часть должна легко обновляться и быть всегда стабильной.


Легкость поддержки сотрудниками сервиса АТ должна быть на уровне я не сильно разбираюсь в специфике проекта вашей команды, но в том, как работает фреймворк тестирования Tladianta, я вам помогу на 100%.


3. Охват основных платформ разработки

В банке много разного продуктового ПО как для клиентов, так и для самого банка и, следовательно, много команд это ПО разрабатывающих либо поддерживающих. Классически это ПО разработано на таких платформах как:


  • Web (HTML, CSS, JavaScript),
  • Desktop (Delphi, C++ и прочие),
  • Mobile (Android, iOS),
  • Back-end(REST-services, MQs очереди).

Фреймворк должен позволять писать тестовые сценарии для любой из них, оставаясь при этом в процессе Tladianta


4. Простота наращивания функционала фреймворка

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


При этом изменение и доработка функционала фреймворка не должны приводить к поломке уже написанных на нем тестов в командах.


5. Возможность настройки под команду или под систему.

Вот мы пришли в команду с чемоданом счастья, прошли Get started и начали писать тесты сразу, используя функционал из коробки.


Это в идеале.


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


Все это должно работать и при этом оставлять проект с использованием нашего фреймворка на поддержке командой сервиса АТ.


6. BDD стиль в написании сценариев

Что в BDD клёво, так это человеко-читаемый и одновременно авто-исполняемый текст сценария. Важно, чтобы все члены команды могли прочитать сценарий и понять, какую часть функционала протестировали.


BDD каким он задумывался: аналитик пишет сценарий, разработчик его воплощает, а автоматизатор делает его авто-исполняемым.


В банке же реальность это различные гибридные варианты. Например:


  • ПО куплено из коробки или разработано вендором, а команде нужно контролировать основные сквозные сценарии;
  • команда разработки ПО в банке и команда тестирования это две разные команды. Ситуация немного похожа на первую, только разработка внутри банка. Так, команда тестирования создает свои e2e тесты, чтобы контролировать основные сценарии и свои доработки;
  • Legacy ПО, которое взяли на поддержку и стали покрывать тестами.

То есть это уже Behavior Driven Testing когда разработка уже давно завершена. А вот прелесть человеко-читаемого описания поведения сложно переоценить и этот подход нужно учесть наравне с классическим BDD.


Значит, фреймворк должен говорить на языке Gherkin.


7. Единая библиотека шагов сценария

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


Чтобы описать атомарные действия пользователя в любой тестируемой системе, нужно не так много шагов, как может показаться с первого взгляда. Сложности скорее в том, как шаг будет реализован для конкретной системы или платформы, а не в том, как эти шаги должны быть сформулированы.


То есть нам нужно иметь 80% всех необходимых шагов, чтобы можно было сразу начать писать сценарии, а оставшиеся 20% шагов добавлять под специфику проекта.


8. Возможность делиться шагами с другими командами

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


Вот эту другую систему поддерживает другая команда, которая пишет для нее свои тесты. На нашем фреймворке. То есть все простые действия, которые нужно сделать в их системе, эта команда уже автоматизировала.


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


9. Гибкий механизм конфигурирования проекта с тестами

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


10. Одновременное использование нескольких платформенных модулей

Нужно охватить еще такой случай. Допустим, команда поддерживает и тестирует некий портал и в сценариях зачем-то нужно сходить в back-end, например, через REST проверить результат или через него же подготовить данные (вдруг с разработкой все по науке сделано :-) ).


Другими словами, тестируем Web и нужно не отходя от кассы что-то сделать в REST. За обе эти части отвечают разные модули, и нужно иметь возможность использовать их одновременно в сценарии.


11. Удобство в расследовании падений и сбора результатов

Это требование про сервис отчетности. Должно быть удобно:


  • взглянуть на отчет о прогоне конкретного сценария и сразу понять, где и что сломалось, какая часть функционала отвалилась. Быть в состоянии это понять должен любой член команды в смысле ясности изложения самого сценария и отображения тестовой модели;
  • провалиться глубже в лог, посмотреть картинки, видео и другие вложения и понять, почему сценарий упал, чтобы потом исправить то, что поломалось;
  • перезапустить сценарий и/или зависимый регресс после расследования и исправления, прямо не отходя от кассы отчета.

12. Удобство e2e тестирования

Об этом было понемногу в пунктах выше, но некоторые моменты стоит выделить отдельно. BDD ориентированность фреймворка подразумевает сценарные тесты, а они бывают в основном функциональные и end-to-end.


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


Да, следует максимально делать независимые тесты, но в случае, если иначе никак, создание и прогон таких сценариев должны быть максимально безболезненными. Заглушки это благо, но не всегда они есть и часто создать их невозможно.


13. Увеличение вовлеченности автоматизаторов на местах в развитие инструмента

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


14. Единая отчетность, сбор метрик и легкое формирование Dashboards

В процессе трансформации в банке образовалось много Agile-команд, которые, тем не менее, все еще работают на один результат все ПО банка должно работать слаженно, в идеале без ошибок и приносить удовлетворение клиентам.


Чтобы избежать ситуации кто в лес, кто по дрова и делать выводы о здоровье ИТ-экосистемы в целом, необходим способ агрегации данных о качестве ПО и о том, насколько автоматизированное тестирование в этом помогает.


Первый шаг к этому сделать общие правила сбора статистики прогонов и расчета на их основе метрик. Фреймворк должен давать возможность внедрения методик, которые помогут с формированием Dashboards и покажут результаты автоматизированного тестирования наглядно и для всех.


15. Использование в Agile командах

BDD задумывался его авторами для облегчения взаимодействия в команде трех ролей:


  • аналитик/owner знает, что должно получиться;
  • разработчик знает, как сделать то, что хочет owner;
  • тестировщик знает, как проверить то, что сделает разработчик, чтобы оно работало как хочет owner.

В команде они в тесном общении создают качественный продукт.


Фреймворк должен быть удобен как инструмент в этом взаимодействии: он должен говорить на языке ownerа, быть понятным разработчику и удобным для тестировщика.


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


Архитектура


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


Плагины предоставляют функционал и реализацию шагов для конкретного стека технологий (платформы), на которых написано тестируемое приложение.


Плагинов в семействе компонентов фреймворка пока 4:


  • HTML-plugin для тестирования веб-приложений, сайтов, порталов и пр. на различных браузерах;
  • Desktop-plugin для тестирования desktop-приложений, написанных на Swing, AWT, .Net и пр. языках высокого уровня, Windows-приложения, а также терминальные. Например, Bank Information System;
  • Mobile-plugin для тестирования мобильных приложений на iOS/Android;
  • Rest-plugin для тестирования REST API приложений любых типов.

Ассортимент и функционал плагинов может (и будет) расширяться. Плагин должен отвечать небольшим требованиям, чтобы иметь возможность работать под управлением "common".


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


Какие задачи решает:


  • охват основных платформ разработки;
  • простота наращивания функционала фреймворка;
  • одновременное использование нескольких платформенных модулей;

Конфигурирование


Запуск тестов на выполнение, как правило, конфигурируется различными свойствами. Эти свойства имеют различные области (уровни) влияния.
Так, например, можно выделить:



Кроме того, часто нужно задать значения свойств любого уровня специально для среды запуска: DEV, TEST, CERT, а также опционально добавлять к набору свойств конфигурации группу свойств под конкретный тест или группу тестов (настройки подключения к базе данных, стороннему сервису и прочие).


В Tladianta реализован механизм сборки свойств конфигурации, а также предусмотрена возможность установки собственного механизма сборки свойств, более подходящего для проекта. Единственное требование свойства уровня фреймворка и плагина должны присутствовать и они должны быть инициализированы.


По умолчанию конфигурация собирается следующим образом:



Загрузка свойств происходит по принципу "каждый следующий затирает предыдущего". Перезапись значения свойства происходит по имени свойства.
Такой подход дает возможность переопределять свойства уровня фреймворка и/или, плагина в проекте, в свойствах среды и т.д.


Какие задачи решает:


  • легкий старт и удобство настройки;
  • Возможность настройки под команду или под систему;
  • гибкий механизм конфигурирования проекта с тестами;

Контекст исполнения сценария


Контекст здесь означает закешированные Java-сущности, которые устанавливаются при старте каждого сценария и обеспечивают корректное исполнение его шагов.
Контекст состоит из


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


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


Такой подход позволил заложить удобство при переходе на контекст другого проекта и использование его шагов прямо в том же самом сценарии (см. далее).


Какие задачи решает:


  • возможность делиться шагами с другими командами;
  • удобство e2e тестирования.

Гостевой модуль использование шагов и контекста других проектов.


Допустим, две независимые команды разрабатывают авто-тесты для двух различных приложений, создают свои уникальные шаги и java-сущности (страницы, элементы и пр.), а также переопределяют методы шагов из библиотеки фреймворка.


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


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


Я прекрасно понимаю гнев и негодование BDD-евангелистов, но что есть, то есть.


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


Для реализации такой возможности Tladianta имеет спец шаги переключения контекста.

Вот тут и пригодились те самые отложенные действия.


Сценарий при этом может выглядеть так:



Каждый проект на базе Tladianta должен иметь свойство конфигурации app.name с понятным человеку именем. Это имя идентифицирует контекст проекта и используется для переключений/установки контекста в сценарии.


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


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


  1. проект тестирования должен быть построен на базе Tladianta-framework;
  2. проект тестирования должен собираться по релизам и публиковаться в банковский репозиторий nexus;
  3. проект подключается как гостевой через зависимость в POM-файле принимающего проекта. Последний стабильный релиз;
  4. проект тестирования в процессе сборки релиза должен включать ресурсы с файлами свойств конфигурации.

Таким образом, все проекты тестирования объединяются в единую экосистему тестирования, подчиненную общим правилам, что значительно упрощает их поддержку силами отдела сервиса АТ, одновременно сохраняя индивидуальность каждой команды.


Какие проблемы решает:


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

Библиотека шагов.


Tladianta реализует принцип "Создай шаг один раз и переопределяй его метод, где нужно".
Шаги, составляющие библиотеку, разбиты на несколько уровней:


  • общие шаги для всех проектов на базе любых плагинов;
  • шаги плагина;
  • шаги проекта.

Вместо классической реализации шагов cucumber Tladianta разделяет определение шага (step definition) и реализацию шага (step implementation) на две разные сущности.
Это позволило отделить формулировки шага и собрать их в библиотеку, оставив плагинам и проектам работу над реализацией конкретного поведения шага.


Таким образом, одна формулировка шага может иметь разную реализацию в разных плагинах и проектах.


Создавая сценарий, больше не нужно беспокоиться о том, что шаг "занят" в другом плагине и нужно дублировать его у себя, меняя формулировку или вставляя разные костыли в виде символов точек, лишних пробелов и так далее, чтобы избежать ошибки "Multiple step definition found".


Вместо этого нужно просто переопределить метод этого шага в своем проекте или даже на конкретной странице.


Внешнее поведение шага, интуитивно ожидаемое от его формулировки, останется тем же, скрывая под капотом нюансы его реализации в каждом конкретном случае.


Мы постарались собрать в библиотеку те самые 80% шагов, которые позволят сразу из коробки написать сценарий средней сложности.


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


Какие задачи решает:


  • возможность настройки под команду или под систему в части реализации шагов;
  • BDD стиль в написании сценариев;
  • единая библиотека шагов сценария;
  • увеличение вовлеченности автоматизаторов на местах в развитие инструмента.

Тестовые данные и шаблоны генерации значений параметров шагов


Cucumber дает возможность часть текста шага использовать как параметр. Это очень удобно.
Tladianta расширяет эту возможность динамической параметризацией шагов.


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


Tladianta перед выполнением сценария все шаблоны заменит на конкретные значения из тестовых данных и в отчетах о прогоне вы увидите уже их.


В качестве источников тестовых данных могут использоваться:


  • Json
  • Excel
  • Properties
  • Mongo DB

Генераторы значений


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


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


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


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


Какие проблемы решает:


  • возможность настройки под команду или под систему;
  • BDD стиль в написании сценариев.

Древовидная структура


Продолжаем тему длинных сквозных сценариев.
Тут есть дилемма атомарные шаги или читаемый сценарий.


Правильно это и то, и другое.


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


Обычно такие сценарии подлежат рефакторингу разбивке на части и упрощению. Беда в том, что после разбивки, такие части сами по себе теряют смысл.


Например, у нас есть сценарии из 700+ шагов. Это большие такие сквозные сценарии. В функциональных тестах отдельные части протестированы. А в этом e2e сценарии ничего не выкинуть. Только читать и поддерживать его невозможно. Что делать?


Мы сделали сценарий древовидным.


Он остается плоским с точки зрения cucumber, а вот с точки зрения человека он выглядит, как дерево. Атомарные шаги сгруппированы, а группы названы объединяющей фразой. Это такие комплексные толстые шаги, которые не шаги, их не нужно переиспользовать, т.к. они как шаги не существуют.


Например,



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



Вы заметили, что это уже не cucumber шаги?


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


Древовидное отображение сценария мы сделали в среде разработки и в отчете Allure.


Узлы дерева можно сворачивать и читать сценарий по верхам.


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


Какие задачи решает:


  • BDD стиль в написании сценариев;
  • удобство в расследовании падений и сбора результатов;
  • удобство e2e тестирования.
  • использование в Agile-командах

Page Object


Мы выбрали шаблон PageObject для архитектуры автотеста как основной, но не единственный. Просто он объектно-ориентированный, это так по javaвски.


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


Вкратце PageObject предполагает описание формы или экрана приложения как отдельного java-класса, а поля класса это части интерфейса формы: поля ввода, кнопки, флажки, таблицы и прочее.


В сценарии специальным шагом

объект страницы устанавливается как текущий и дальнейшие шаги работают в контексте этой страницы.


Блоки.


Страницы бывают сложные: много вкладок, разных сгруппированных полей и часто у этих полей одинаковые относительные пути (xPath), что наводит на мысль о DRY, переиспользовании, ООП в конце концов.


Логично выделить их в отдельную сущность и вообще разбить страницу на группы, области.


Однако группа полей логически не является страницей.
Поэтому было введено понятие блоков это такие специальные классы, не являющиеся страницей в парадигме PageObject, единственная цель которых облегчение класса страницы и переиспользование группы полей на страницах проекта.


Когда группа полей визуально выделена на странице, удобно использовать навигацию вида хлебные крошки для обращения к элементу страницы из шага сценария.


Например, на странице есть два повторяющихся элемента: "Номер" и "Остаток". Они присутствуют на странице в двух местах: "Данные счета" и "Данные карты".
В этом случае элементы "Номер" и "Остаток" нужно вынести в отдельный класс-блок, а на страницу добавляем два элемента-блока "Данные счета" и "Данные карты".
В шаге обратиться к элементу "Остаток" для разных блоков можно следующим образом:



Блоки элементов могут быть вложены друг в друга



Блоки могут быть списками



Такая навигация очень помогает ориентироваться в сценарии и приложении.


Части пути к элементу должны быть названы так, как они выглядят на форме/странице. То есть, читая сценарий, можно сразу глазами находить элемент на экране и понимать, что куда заполняется и где нажимается.


Довольно удобно.


Какие задачи решает:


  • BDD стиль в написании сценариев.
  • удобство в расследовании падений и сбора результатов.
  • использование в Agile командах всем просто понять сценарий

Использование нескольких плагинов в одном проекте


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


В этом случае функционал переключения контекста не имеет смысла, т.к. контекст проекта общий для обоих плагинов. Это нужно учитывать при создании java-сущностей (страниц, блоков, шагов и т.д.).


Самая распространенная ситуация это когда нужно результат действий через UI проверить через REST запросы к бэкенду приложения.


Какие задачи решает:


  • одновременное использование нескольких платформенных модулей;

Запись видео.


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


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


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


Какие задачи решает:


  • возможность настройки под команду или под систему.
  • гибкий механизм конфигурирования проекта с тестами.
  • удобство в расследовании падений.

Allure-report


По-умолчанию результаты прогона тестов логируются в Allure-отчет. Логирование поддерживает древовидную структуру сценария, если она используется. В отчет попадают также логи, сделанные с помощью Sl4j.


Allure позволяет группировать сценарии по Epic/Feature/Story/Case. Это полезно, если у вас есть (должны быть) тестовая модель и матрица зависимости. И когда просматриваешь отчет, который сгруппирован так, как у вас модель тестовая составлена, то становится очень удобно, т.к. по упавшему тесту в отчете сразу видно, какой связанный функционал пострадал и потенциально стал нерабочим.


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


Все результаты собираются в одном месте по проектам/командам и вместе дают ту самую целую картину здоровья ИТ-ландшафта. Причем, собираются также результаты тестов, которые написаны не на Tladianta.


Это уже не фреймворк Tladianta (он только генерирует данные), это уже процесс Tladianta.


Какие задачи решает:


  • BDD стиль в написании сценариев.
  • удобство в расследовании падений.
  • единая отчетность, сбор метрик и легкое формирование Dashboards.
  • Использование в Agile командах

Передача данных между шагами и сценариями.


Идеология cucumber настаивает на том, что каждый шаг сценария по своему поведению должен быть независимым и самодостаточным (атомарным). То же относится и к сценарию.


Однако бывают случаи, когда атомарность шага немного мешает и нужно иметь некий временный кэш сценария.


Таким кэшем во фреймворке выступает временный объект контекст сценария. Он позволяет в одном шаге сохранить какое-либо значение в переменную, а в следующем шаге использовать его.


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


Кроме того, данный контекст можно закрепить за конкретным сценарием и сохранить его в базе данных. Опционально. Для этого есть отдельные методы, их можно использовать в Before/After-хуках в проекте.


По-умолчанию контекст сценария очищается перед каждым сценарием и не сохраняется в базу данных.


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


Какие задачи решает:


  • удобство e2e тестирования;

Шаблонные проекты.


Это часть семейства инструментов Tladianta.


Мы сделали заготовки, шаблоны проектов, которые настроены на работу с каким-то одним плагином. Сделали в них структуру со всеми ключевыми пакетами и классами. Сделали настройку для фичи гостевой проект, для выгрузки отчета Allure.


Просто делай клон, пиши имя проекта и начинай писать тесты.


В проектах можно кастомизировать почти все, что может фреймворк:


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

Какие задачи решает:


  • легкий старт и удобство настройки;
  • охват основных платформ разработки;
  • возможность настройки под команду или под систему;

Интеграция с внешними сервисами и инструментами.


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


Мы сделали репортер в ALM и репортер в Jira.


Jira-reporter, например, создает задачу на реализацию новых шагов на автоматизатора в команде, а также задачу на расследование падения сборки со всеми ссылками и логами.


Tladianta-editor


Это плагин для среды разработки IntelliJ IDEA.


Его основное назначение повысить скорость и комфорт в разработке сценария.


Можно создать рабочий сценарий не выходя из файла .feature, при условии что использован шаблонный проект, конечно.
Он позволяет:


  • выбрать шаг из доступной библиотеки шагов
  • перейти из шага сразу в его step definition (это должно быть знакомо)
  • позволяет создать заглушку step definition для нового шага (это тоже)
  • выбрать страницу из списка в шаге установки текущей страницы
  • а также создать новую страницу с элементами и блоками, не уходя из редактирования сценария
  • провалиться в класс страницы, если уж так сильно нужно.
  • набивать стрелочный путь до элемента в блоке, который в блоке и т.д.
  • выбирать шаги гостевого проекта после переключения на его контекст
  • выбирать страницы гостевого проекта после переключения на его контекст
  • редактировать сценарий как дерево, добавляя и удаляя группировки шагов. Уровень вложенности любой
  • и прочее.

Editor позволяет писать сценарий быстро, используя все возможности фреймворка и сводя к минимуму возню с рутинным кодом.


Реализацию нового шага или специфичную реализацию существующего шага придется все-таки писать самостоятельно. Но для этого есть автоматизатор :-)


Заключение


Как видите, мы нарушили много канонических положений BDD. При этом мы сделали cucumber пригодным для использования в наших банковских реалиях. Получился такой cucumber по-росбанковски. Поэтому и Tladianta.



Не все идеи мы придумали сами. Многие из них мы почерпнули из трудов таких замечательных ребят как Константин Мальцев, Виктор Сидоченко, Алексей Лустин, Артем Ерошенко и других авторов open-source проектов.


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

Подробнее..

Как мы реализовали параллельный запуск Cucumber-сценариев на разных хостах

11.02.2021 22:07:58 | Автор: admin

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

Наш тестовый фреймворк реализован на базе Cucumber (Java). Основной фокус автоматизация GUI тестирования Swing-приложений (используем AssertJ Swing), а также взаимодействие с БД (SQL), REST API (RestAssured) и web UI (Selenide).

Ситуация осложнялась тем, что подключить параллелизацию стандартными средствами (например, настройкой потоков в maven-surefire-plugin) мы не могли, так как специфика тестируемых приложений не позволяет запускать более одного экземпляра приложения на одном физическом хосте.

Кроме того, требовалось учесть, что некоторые сценарии используют общие внешние ресурсы, которые не поддерживают параллельные запросы.

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

Обзор реализации

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

Компоненты схемы:

  • Master-хост, на котором запускается модуль Dispatcher, управляющий созданием очереди сценариев для параллельного запуска и последующей агрегацией результатов.

  • JMS server (для простоты развертывания мы поднимаем свой ActiveMQ Artemis, встроенный в Dispatcher).

  • Горизонтально масштабируемые agent-хосты, на которых запускается FeatureExecutor, непосредственно выполняющий сценарии из очереди. На каждый agent предварительно должен быть установлен тест фреймворк.

Master-хост при этом может одновременно выполнять роль одного из агентов (для этого запускаем на нем и Dispatcher, и FeatureExecutor).

Dispatcher при старте получает на вход директорию с features, считывает список файлов, и отправляет их в JMS очередь сценариев.

FeatureExecutor считывает сценарий из очереди и отправляет команду в Test Framework на выполнение этого сценария.

Как получить общий отчет по результатам прогона со всех хостов?

Как мы знаем, Allure складывает файлы с результатами по каждому сценарию в директорию resultsDir (по умолчанию это build/allure-results).

Так как мы уже используем JMS для создания очереди сценариев, закономерным решением мы посчитали добавление еще одной очереди для передачи результатов.

FeatureExecutor после каждого сценария архивирует содержимое resultsDir и в виде BytesMessage отправляет в эту очередь, откуда их считывает Dispatcher и распаковывает в resultsDir уже на мастер-хосте.

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

Что, если один из агентов упал?

Диспетчер завершает работу, когда обработал все результаты (сравниваем поштучно количество отправленных сценариев VS количество полученных результатов). Если один из агентов забрал сценарий из очереди, но не отправил результат, можно получить зависший прогон, так как диспетчер будет бесконечно ждать этот результат

Для решения этой проблемы мы добавили в Dispatcher сервис HealthChecker, который в фоновом потоке через RMI-интерфейс по шедулеру вызывает метод isAlive() на каждом зарегистрированном агенте (список агентов мы загружаем из конфигурации на старте).

Реализация метода на агенте примитивна:

@Overridepublic boolean isAlive() {return true;}

Если связь хотя бы с одним агентом была потеряна, прогон завершается.

Такое условие было реализовано по принципу минимальных вложений в первой версии нашего механизма.

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

Альтернативные опции:

  1. По аналогии с RMI использовать REST API.

  2. Создать отдельную JMS очередь для сообщений типа heartbeat, которые будут отправлять агенты. А на стороне Dispatcher проверять, что от каждого агента мы получаем минимум один heartbeat в 5-10-30 минут (конфигурация по вкусу).

Как быть с тестами, которые не могут выполняться параллельно?

Например, использующие общие ресурсы.

Наше решение стилизовано под специальный Cucumber тег в feature-файле @NoParallel. Почему стилизовано? Потому что читаем мы этот тег средствами java.nio.file, а не через Cucumber. Зато он красиво вписывается в файл сценария:

@NoParallel(PrimaryLicense)@TestCaseId("876123")Scenario: 001 Use of shared resource

Если два сценария используют общий ресурс (обозначенный в теге @NoParallel) они не должны запускаться параллельно.

Для управления ресурсами у нас реализован ResourceManager на стороне Dispatcher с двумя методами в API:

public boolean registerResource(String resource, String agent);public void unregisterResource(String resource, String agent);

Когда FeatureExecutor получает сценарий из очереди с тегом @NoParallel, он вызывает метод registerResource().

Если метод возвращает true ресурс успешно зарегистрирован, продолжаем выполнение сценария.

Если получаем false ресурс занят. Сценарий возвращается в очередь.

После выполнения сценария ресурс освобождается через unregisterResource().

Взаимодействие с ResourceManager у нас реализовано через RMI.

Как вариант, можно использовать REST API.

Summary

  1. Данное решение стоило нам 0 изменений в тестовом фреймворке.

  2. Как следствие имеем полную обратную совместимость. Автотесты могут запускаться и старым, и новым способом без изменений в коде.

  3. Не нужно заботится о потокобезопасности тестов. Они изолированы, поэтому нет разницы, запускаются они параллельно или нет.

  4. Агенты горизонтально масштабируются, легко ускорить выполнение тестов до определенного предела.

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

Подробнее..

Перевод Нестабильные(Flaky) тесты одна из основных проблем автоматизированного тестирования

26.04.2021 10:24:01 | Автор: admin

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

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

Данная статья призвана рассказать как бороться с каждой из причин.

За прошедшие годы я не раз сталкивался с нестабильными тестами, но вместо того чтобы рассматривать конкретные случаи, давайте попробуем сгруппировать причины возникновения нестабильности по задействованным при выполнении автотестов компонентам:

  • Сами тесты;

  • Фреймворк для запуска тестов;

  • Сервисы и библиотеки, от которых зависит тестируемая система и тестовый фреймворк;

  • Операционная система и устройство с которым взаимодействует фреймворк автотестирования.

На рисунке 1 показан стек оборудования / программного обеспечения, который поддерживает тестируемое приложение или систему. На самом низком уровне находится оборудование. Следующий уровень - это операционная система, за которой следуют библиотеки, обеспечивающие интерфейс с системой. На самом высоком уровне находится промежуточное программное обеспечение, уровень, который предоставляет интерфейсы для конкретных приложений.

Однако в распределенной системе каждая служба приложения и службы, от которых она зависит, могут находиться в другом аппаратном / программном стеке, как и служба, выполняющая тест. Это проиллюстрировано на рисунке 2 как полная среда выполнения теста

Как обсуждалось выше, каждый из этих компонентов является потенциальной областью нестабильных тестов

Сами тесты

Сами тесты могут вызвать нестабильность. Типичные причины:

  • Неправильная инциализация или очистка;

  • Неправильно подобранные тестовые данные;

  • Неправильное предположение о состоянии системы. Примером может служить системное время;

  • Зависимость от асинхроных действий;

  • Зависимость от порядка запуска тестов.

Фреймворк для запуска тестов

Ненадежный фреймворк для запуска тестов может привести к нестабильности. Типичные причины:

  • Неспособность выделить достаточно ресурсов для тестируемой системы, что приводит к ее сбою;

  • Неправильное планирование тестов, поэтому они "противоречат" и приводят к сбою друг друга;

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

Сервисы и библиотеки, от которых зависит тестируемая система и тестовый фреймворк

Приложение (или тестируемая система) может быть источником нестабильности

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

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

Типичные причины:

  • Состояние гонки;

  • Непроинициализированные переменные;

  • Медленный ответ или отсутствие ответа при запросе от теста;

  • Утечки памяти;

  • Избыточная подписка на ресурсы;

  • Изменения в приложении и в тестах происходят с разной скоростью.

Среды тестирования называются герметичными, если они содержат всё, что необходимо для запуска тестов (т.е. нет внешних зависимостей, таких как серверы, работающие в прод окружении).

Герметичная среда менее подвержена нестабильности.

Операционная система и устройство с которым взаимодействует фреймворк автотестирования

Наконец, оборудование и операционная система могут быть источником нестабильности тестов. Типичные причины включают:

  • Сбои или нестабильность сети;

  • Дисковые ошибки;

  • Ресурсы, потребляемые другими задачами / службами, не связанными с выполняемыми тестами.

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

В следующих статьях мы рассмотрим способы решения этих проблем.

Ссылки на источники

Подробнее..

Recovery mode Способыхраненияданныхвавтотестах и автоматизациятестированияПО вавионке что будет на LoGeek Night QA

07.06.2021 18:08:54 | Автор: admin

17 июня в 18:00 состоится Online LoGeek Night QA. На нем наши тестировщики расскажут о плюсах и минусах разных способов хранения данных в автотестах с примерами на Java, а также об автоматизации высокоуровневого тестирования ПО в авионке.

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

Программа

18:00 18:50 Александр Гвоздев

Автоматизация высокоуровневого тестирования ПО максимального уровня безопасности для авионики (приборы первичной индикации, уровни A и B).

В докладе Александр рассмотрит:

Категории критичности отказных состояний бортового ПО и их влияние на аспекты сертификации;

Особенности верификации ПО максимального уровня безопасности;

Реализацию тестирования ПО первичной индикации;

Автоматизацию высокоуровнего тестирования ПО первичной индикации

О спикере: тестировщик по призванию. С 2000 года занимаюсь тестированием бортового ПО авиационного, аэродромного, автомобильного и даже подводного, и жизни многих людей зависят от качества моей работы. Рад, что внёс (пусть даже относительно крошечный) вклад в прогресс всего человечества.

18:50 19:40 Максим Власов

Способы хранения тестовых данных.

В докладе Максим расскажет о:

Двух способах хранить данные в авто тестах: данных в виде хардкода в файлах (json, xml, csv) и модуле внутри фреймворка, который данные генерирует;

О минусах и плюсах этих подходов с примерами на Java.

О спикере: большой опыт тестирования web приложений. В роли автоматизатора QA 5 лет. Два самых крупных проекта Yandex, реклама и Luxoft, банковская сфера. Прошёл путь от ручного тестировщика до тест лида. Любит рассказывать про сложные вещи простым языком.

19:40 20:00 Розыгрыш призов

Как принять участие

Митап пройдет онлайн 17 июня, 18:00 (МСК).

Чтобы принять участие, нужно:

  • Зарегистрироваться насайте;

  • Перейти по ссылке в ZOOM, которую вы получите за сутки и за час до начала митапа на почту.

Будем рады вас видеть!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru