Предисловие

Привет, Хабр! В данной статье-мануале я хочу рассказать о базовых функциях такого фреймворка как Cucumber и его применение для создания UI-автотестов на мобильных iOS устройствах.

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

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

Несколько слов о Gherkin, Cucumber и BDD

Gherkin представляет из себя структурированную манеру написания документации для PO, бизнес-аналитиков и тестировщиков. «Академическое» определение Gherkin говорит, что это человеко-читаемый язык для описания поведения системы, каждая строчка начинается с одного из ключевых слов (Given-When-Then-And) и описывает одно из предусловий/шагов/результатов.

Cucumber - это инструмент, используемый для выполнения behavior driven (BDD) сценариев, написанных на языке Gherkin.

BDD (behavior driven development) - методология разработки ПО, основной идеей которой является составление требований к продукту в формате понятых не-специалисту поведенческих сценариев. Пример BDD сценария:

Scenario: Login with PIN
Given the app is running
And I'am registered user
And I see Login screen
When I enter 4-digits PIN
Then I am logged in

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

Разворачиваем Cucumber

На сайте Cucumber поддержка iOS находится в любопытном статусе semi-official (полу-официальный?) поэтому процесс установки и настройки фреймворка местами весьма нетривиален.

  • Cucumber - это СocoaPod библиотека, поэтому начинаем с нее. Открываем терминал и устанавливаем CocoaPod

    sudo gem install cocoa pods

  • переходим в свой проект и создаем подфайл

    pod init

  • заходим в созданный подфайл. Проверяем, что в нем есть следующее:

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '11.0'

use_frameworks!
inhibit_all_warnings!

def test_pods
  pod 'Cucumberish'
end

target ‘НАЗВАНИЕ_ВАШЕГО_ПРОЕКТАCucumberTests’ do
  test_pods
end

Если этой записи нет - добавляем её руками, предварительно удалив всё из файла. Обратите внимание, что в таргете фигурирует название вашего проекта

  • устанавливаем XCFit

    sudo gem install xcfit

  • открываем Xcode, добавляем к проекту новый таргет, используя появившийся шаблон cucumberish  bundle. В проекте появятся папка НазваниеВашегоПроектаCucumberTests с дефолтными файлами. Там же необходимо самостоятельно создать папку Features

  • идем в build phases таргета CucumberTests, удаляем дубликаты Copy Bundle Resources, Compile Sources и Link Binary With Libraries

  • закрываем Xcode, устанавливаем поды

    pod install

  • в проекте появится .xcworkspace файл, открываем его. Зачастую на этом шаге отваливаются стандартные файлы из папок Screens, Step Definitions и Supporting Files. Если это произошло - добавляем их обратно руками через Add Files to.

    Почти готово, мы в шаге от завершения установки Cucumber!

  • на момент написания этой статьи (осень 2020) «из коробки» фреймворк не собирается из-за нескольких ошибок в коде. Возможно, эти проблемы будут неактуальны. Через поиск в Xcode находим и правим следующие строки:

    • добавляем @objc перед class func CucumberishSwiftInit()

    • зменяем var elementQurey на var elementQuery

    • заменяем expectedMenuCount: uInt = uInt(menuCount)! на expectedMenuCount: Int = Int(menuCount)!

    • заменяем expectedCellCount: uInt = uInt(cellCount)! на expectedCellCount: Int = Int(cellCount)!

Вот теперь все. Жмем ?+U для запуска тестов. Если все сделано правильно, проект соберется, а в логе мы увидим сообщение «Tests successfully executed 0 from 0». Это значит, что фреймворк успешно отработал, но не запустил ни одного теста, что не удивительно, так как мы еще не написали ни одного сценария.

Фреймворк "из коробки"

По умолчанию Cucumber имеют следующую структуру:

  • папка Features - в ней хранятся файлы с разрешением .feature, они содержат в себе один или несколько BDD сценариев. Каждый сценарий, написанный на Gherkin, состоит из набора строк, последовательно выполняемых при работе теста

  • <название проекта>CucumberTests.swift файл в корне фреймворка. Содержащий код, выполняемый для Given шагов, задает предусловия каждого теста. По умолчанию файл будет содержать шаг для запуска приложения. Также файл содержит ряд настроек, к примеру, управление тэгами, но о них чуть позже

  • папка Screens - в ней хранятся .swift файлы, содержащие локаторы, по которым можно обращаться к инспектируемым элементам приложения

  • папка Step Definitions - в ней хранятся .swift файлы содержащие код, выполняемый в шагах тестов

  • папка Common - в ней хранится commonStepDefinitions.swift файл, содержащий в себе код, который может выполняться для простых проверок типовых элементов (наличие объектов на экране, нажимаемость кнопок и т.п.)

Для удобства организации шагов и сценариев в соответствующих папках фреймворка можно создавать дополнительные файлы. Все .feature файлы, помещенные в папку Features, будут подтягиваться в тестовые наборы автоматически, а файлы с шагами потребуется дополнительно вызывать в beforeStart функции CucumberishSwiftInit()

Что происходит при запуске теста?

Фреймворк установлен, теперь мы хотим создать свой первый тест. Для того, чтобы это было проще сделать, надо понимать, что будет происходить c Xcode после того, как вы нажмете ?+U.

А происходить будет следующее:

  • фреймворк соберет проект на выбранном девайсе/эмуляторе

  • фреймворк построчно распарсит первый сценарий из первого фича-файла

  • все первые Given и And строки будут рассматриваться как предусловия теста. Они могут быть как совсем простыми и состоять из единственного Given условия, так и комплексными Given-And-And-… наборами строк

Простой шаг-предусловие
Простой шаг-предусловие
  • фреймворк отправится в корневой swift файл за кодом, выполняемым в этих шагах. Если точнее, то для простого предусловия выше будет выполнен следующий код:

Шаг «The app is running» является первым и обязательным почти для любого сценария, так как в нем происходит запуск трестируемого приложения.

  • как только будут пройдены все Given шаги и дело дойдет до первой When или Then строки, предусловия будут считаться выполненными. За кодом для выполнения последующих строк фреймворк будет обращаться в .swift файлы из папки Step Definitions

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

Пишем свой первый тест

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

  • создаем feature файл в папке Features. В начале файла обязательно должна быть общая информация описывающая функционал по шаблону:

Feature:…
In order to …
As a …
I want to … 
  • пишем тестовый сценарий в feature файле, используя синтаксис Gerkin

  • в корневом swift файле раскрываем Given шаги этого сценария

  • в step definition файле раскрываем остальные шаги

Особенности написания Given шагов

Упомянутого выше предусловия «the app is running» самого по себе почти всегда будет недостаточно. Если точка входа в тест находится не на стартовой страничке, а где-то «глубже», то удобно использовать комплексные Given-And-And-… предусловия. При помощи еще одной или нескольких строк, мы можем привести приложение к нужному для конкретного теста состоянию.

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

Итак, из предусловия сценария видно, что после запуска приложения я хочу взаимодействовать с ним как зарегистрированный пользователь, хочу видеть экран «My credentials», и при этом экран должен быть пустым и не содержать ни одной сущности.

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

  • Запускаем приложение

  • если после запуска мы видим страницу регистрации, то регистрируемся, нажав на кнопку «Get started». Ждем, пока прогрузится экран для установки пинкода, задаем пин «0000», вводим его повторно для подтверждения регистрации

Если после запуска мы видим НЕ страницу регистрации, значит, регистрация была произведена в предыдущем тесте, и приложение, сохранив этот состояние, направит нас сразу на экран логина. В этом случае нам остается лишь залогиниться единожды введя пин «0000».

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

  • Проверяем, что после регистрации/логина приложение прогрузилось, и мы находимся на экране «My credentials». Простой ассерт, в котором мы ожидаем экран

  • Проверяем, что на экране в табличке credentialList нет сущностей, если они остались с предыдущих тестов, удаляем их одну за одной. Для этого тапаем по первой ячейке в таблице, вызываем меню действий, тапаем по кнопке "Delete" и подтверждаем кнопкой "Delete credential". Повторяем эти действия пока первая ячейка в таблице не перестанет существовать

В результате предусловия выполнены. Мы имеем запущенное приложение, ожидающее дальнейших шагов на пустом экране «My credentials».

Несколько слов о тестовых шагах и CommonStepDefinitions

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

Отдельно хочется рассказать про CommonStepDefinitions.swift. Он создается по умолчанию в папке Common и содержит в себе набор проверок, которые могут быть использованы для тестирования стандартных, типовых элементов.

Допустим, в вашем приложении есть условный объект Menu, на нем находятся две стандартные кнопки Confirm и Deny, а также текст «Please confirm». Для того, чтобы проверить отображение этого объекта, кнопок и текста, не обязательно в step definitions детализировать код для четырех разных шагов. Достаточно просто в сценарии написать эти шаги в подходящем под CommonStep формате:

…
And I see the "Menu" view
And I see "Confirm" button
And I see "Deny" button
And I see "Please confirm" text
…

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

Использование common шагов позволяет здорово сократить количество кода, необходимого для простых проверок или действий. Но надо понимать, что этот метод не всегда подходит для тестирования кастомных элементов из-за сложных путей их обнаружения. В этом случае удобнее всего пользоваться обычными step definitions и обращаться к этим элементам через accessibility identifier.

Запускаем тесты

После того, как один или несколько BDD сценариев написаны и ко всем их строкам существует definition, можно запускать тестовый прогон комбинацией ?+U. В случае, если необходимо запустить только часть тестов, можно воспользоваться тегами, отметив ими желаемые сценарии в фича файлах (или фича файлы целиком) и перечислив теги для запуска в executeFeatures корневого .swift файла.

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

Из этого можно извлечь пользу. К примеру, на нашем проекте из общего количества автотестов в 50+ штук необходимо всего 5 тестов на регистрацию. После прохождения этих 5-и тестов состояние регистрации сохраняется и последующие 45 тестов начинаются уже с логина, минуя десяток тапов по кнопкам и пару экранов. Тем самым, мы экономим примерно 10 секунд времени от каждого теста, не выполняя одни и те же действия, уже протестированные ранее.

Обратная сторона медали - периодически требуется очищать приложение от различных сущностей в предусловиях тестов (как было показано выше на примере шага «I have no credentials»). Для этого приходится писать довольно объемные шаги, но в моем случае такой существенный выигрыш времени стоит того.

Отчет

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

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

Когда начинается магия?

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

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

Если добавить к этому то, что при разработке по BDD методологии сценарии для вас пишет PO и пишет их точь-в-точь как вам надо, то получается совсем утопическая вселенная, где тестировщику остается только скопипастить текст из Jira в фича-файл и получить работающий автотест.

Естественно, реальность всё-таки вносит свои корректировки, и PO будет иметь отличный от тестировщика взгляд на описание фичи, чаще всего, сценарии приходится глубоко редактировать, дополняя деталями или пропущенными шагами, примерять на них стандартные техники тест дизайна. И без добавления кода в step definitions тоже никак не обойтись.

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

Надеюсь, эта статья помогла вам узнать чуть больше о таком не самом распространенном, но очень интересном звере в мире мобильных UI-автотестов как Cucumber. Если у вас есть вопросы, жду их в комментариях, постараюсь все прочитать и на все ответить.