Пишем первый тест на cucumber для Rails

Предисловие

Этот материал создавался скорее как краткое техническое руководство по установке и использованию cucumber для Rails.

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

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

Целью создания cucumber с точки зрения авторов является предоставление инструмента для взаимодействия между заказчиком программного продукта и его создателями. Cucumber позволяет создать общее информационное поле (словарь терминов), в котором описывается желаемое поведение создаваемого продукта (и его частей), которое в последующем транслируется в желаемое поведение системы. (При использовании BDD подхода) Одновременно решается задача наличия актуальной документации по поведению системы, которая автоматически получается из тестов при их выполнении. Внесение изменений в тестах приводит к изменению документации (выполняемых шагов, предварительных действий и т.д), что позволяет иметь всегда актуальную версию.

Активное участие заказчика в составлении и утверждении тестов решает сразу две проблемы разработки.

  1. Система выполняет действия, описанные в терминах заказчика, которые должна выполнять.

  2. Успешное подтверждение тестами позволяет осуществить "приемку" передаваемого продукта как соответствующего заявленным ожиданиям.

Теперь переходим к "техническим" аспектам использования cucumber для Rails 7.

Установка Cucumber для Rails

Установка Cucumber для Rails состоит из следующих шагов.

  • Установить cucumber-rails

  • Установить и настроить RSpec.

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

  • Настроить желаемое поведение cucumber

Устанавливаем Gem

group :test do
  gem 'cucumber-rails', require: false
  # database_cleaner is not mandatory, but highly recommended
  gem 'database_cleaner'
end

Запускаем генератор, который создает необходимые директории и файлы для работы cucumber

% rails generate cucumber:install

По умолчанию cucumber-rails запускает DatabaseCleaner.start и DatabaseCleaner.clean перед и после каждого сценария. Это возможно отключить в файле конфигурации. Отключать категорически не рекомендуется, каждый сценарий желательно выполнять не зависимо как друг от друга, так и от наличия данных в тестовой базе данных.

# features/support/env.rb
# ...
Cucumber::Rails::Database.autorun_database_cleaner = false

По умолчанию будут добавлены helpers Rack::Test.

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

ENV['CR_REMOVE_RACK_TEST_HELPERS'] = 'true'

Работа с RSpec

RSpec устанавливается как описано в RSpec-rails. Дополнительных настроек не требуется.

Примечание

  • Тег @allow-rescue позволяет отключить перехват ошибок Cucumber, если выполняется тестирование перехвата этих ошибок приложением. В ином случае ошибки будут обработаны Cucumber ДО того, как они попадут в выполняемый шаг.

  • Тег @javascript указывает использование javascript драйвера при выполнении сценария(теста). Может устанавливаться как для шага, так и для сценария целиком.

Webdrivers и Capubara

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

  • Selenium WebDriver

  • Watir

  • Capybara (с тем же Selenium WebDriver)

Более подробная информация об установке и использовании на официальном сайте Cucumber.

Будем использовать Capybara, поскольку мы уже используем этот модуль в RSpec

Если Capybara уже установлена для использования с RSpec, можно использовать настройки webdriver, которые были сделаны для нее. Для этого в файл конфигурации features/support/env.rb необходимо добавить конфигурацию для драйвера capybara, применяемую для RSpec

require 'RSpec/rails'
require Rails.root.join('spec', 'support', 'capybara')

В файле spec/support/capybara.rb содержатся настройки для работы Capybara в данном случае с браузером Chrome

Capybara.server = :puma, { Silent: true }

Capybara.register_driver :chrome_headless do |app|
  options = Selenium::WebDriver::Chrome::Options.new

  options.add_argument('--headless')
  options.add_argument('--no-sandbox')
  options.add_argument('--disable-dev-shm-usage')
  options.add_argument('--window-size=1400,1400')

  Capybara::Selenium::Driver.new(app, browser: :chrome, options:)
end

Capybara.javascript_driver = :chrome_headless

# Setup RSpec
RSpec.configure do |config|
  config.before(:each, type: :system) do
    driven_by :rack_test
  end

  config.before(:each, js: true, type: :system) do
    driven_by :chrome_headless
  end
end

Конфигурация запуска тестов cucumber

В файле config/cucumber.yml содержатся конфигурации профилей для запуска тестов. Можно оставить только конфигурацию по умолчанию.

# config/cucumber.yml
default: --publish-quiet
wip: --tags @wip:3 --wip features
prod: --publish-quiet --strict # Использование strict завершает с ошибкой все сценарии и шаги с pending, в ином случае они завершаются без ошибок. Можно использовать для тестов в  CI/CD

Можно создавать дополнительные конфигурации для запуска тестов.

Пример запуска тестов с указанием конфигурации(профиля)

% bundle exec cucumber -p wip

На этом установка может считаться успешной, если при первом запуске cucumber мы получаем следующий результат.

% bundle exec cucumber

0 scenarios
0 steps
0m0.000s

Первый тест feature

Cucumber при запуске ищет и выполняет файлы с расширением feature. По умолчанию данные файлы для Rails расположены в каталоге feature.

Создадим первый сценарий.

Сценарий - это файл, который необходимо расположить в каталоге feature и который имеет расширение .feature

Сценарии могут быть организованы с использованием файловой структуры (подкаталоги).

Cucumber по умолчанию исполняет все файлы с расширением .feature в каталоге feature, включая подкаталоги.

Каталог по умолчанию можно переименовать и указать это в конфигурации(профиле) при запуске.

В начале файла указывается язык, на котором написан данный сценарий.(# language: ru)

Список поддерживаемых языков можно получить из консоли.

% bundle exec cucumber --i18n-languages

Пример файла сценария.

# language: ru
@all
@javascript
Функционал: Создание Классификатора и его заполнение
  Предыстория:
    Дано Пользователь перешёл по ссылке в меню "Классификатор"
    Сценарий: Создание классификатора
        Если Пользователь вводит название классификатора "ОИВ"
        И Нажимает "Создать Классификатор"
        То Создается классификатор с названием "ОИВ"

    Сценарий: Заполнение классификатора разделами
        Если Пользователь вводит название классификатора "ОИВ"
        И Нажимает "Создать Классификатор"
        И Пользователь нажимает на раздел классификатора "ОИВ"
        И Заполняет название раздела после точки "Департамент_Здравоохранения"
        И Нажимает "Создать Классификатор"
        То Создается раздел классификатора с полным названием "ОИВ.Департамент_Здравоохранения"

В заголовке теста указывается язык написания теста, в данном случае - русский

# language: ru

Далее могут идти теги.

@all@javascript

В примере приведены два тега, первый - это ссылка для запуска всех сценариев в этом файле, второй - указывает cucumber, что сценарии при выполнении должны использовать javascript

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

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

%  bundle exec cucumber --tags @test

0 scenarios
0 steps
0m0.000s

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

Структура теста feature

Рассмотрим более детально структуру теста

Функционал:

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

Подробное описание (description) приводится с новой строки ниже. Весь текст до следующего ключевого слова будет рассматриваться как описание и выводится в тесте как элемент документации.

Предыстория

Можно провести некую аналогию с hook before в RSpec. Устанавливает действия, которые должны быть выполнены ДО начала каждого сценария. Если это действие не выполняется, сценарии тоже выполняться не будут.

Используется для объединения действий, общих для всех сценариев и переноса их в этот блок.

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

В предыстории содержится одно или несколько ключевых слов "Дано" - это описание действий, которые должны быть выполнены.

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

Сценарий

Ключевой элемент теста.

Сценарий состоит из начального ключевого слова Если, за которым следуют (могу следовать) слова И,НО и другие, которые в свою очередь описывают последовательность действий пользователя в системе. Завершается сценарий ключевым словом То, в котором проверяется результат, достигнутый после выполнения действий. Проверка результата То также может включать несколько действий, объединенных ключевыми словами.

Сценарий состоит из трех условных частей:

  • Дано (Given)

  • Когда (When)

  • То (Then)

Ключевые слова имеют несколько синонимов. Информацию о соответствии ключевых слов на том или ином языке можно получить следующим образом.

%bundle exec cucumber --i18n-keywords ru

  | feature          | "Функция", "Функциональность", "Функционал", "Свойство" |
  | background       | "Предыстория", "Контекст"                               |
  | scenario         | "Пример", "Сценарий"                                    |
  | scenario_outline | "Структура сценария", "Шаблон сценария"                 |
  | examples         | "Примеры"                                               |
  | given            | "* ", "Допустим ", "Дано ", "Пусть "                    |
  | when             | "* ", "Когда ", "Если "                                 |
  | then             | "* ", "То ", "Затем ", "Тогда "                         |
  | and              | "* ", "И ", "К тому же ", "Также "                      |
  | but              | "* ", "Но ", "А ", "Иначе "                             |
  | given (code)     | "Допустим", "Дано", "Пусть"                             |
  | when (code)      | "Когда", "Если"                                         |
  | then (code)      | "То", "Затем", "Тогда"                                  |
  | and (code)       | "И", "Ктомуже", "Также"                                 |
  | but (code)       | "Но", "А", "Иначе"                                      |

Приведенный пример содержит простой сценарий, в котором исходные данные задаются в тексте условия Если


    Сценарий: Создание классификатора
        Если пользователь вводит название классификатора "ОИВ"
        И нажимает "Создать Классификатор"
        То видит созданный классификатор с названием "ОИВ"

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

Различные действия соединяются с использованием вспомогательных конструкций И и Но

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

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

Пример сценария, где часть ключевых слов заменена на *(звездочки) для некоторого улучшения "читабельности" теста.

   Сценарий: Заполнение классификатора разделами
        Если пользователь вводит название классификатора "ОИВ"
        * нажимает "Создать Классификатор"
        * нажимает на раздел классификатора "ОИВ"
        * вводит название подраздела "Департамент_Здравоохранения"
        * нажимает "Создать Классификатор"
        То видит созданный раздел классификатора с полным названием "ОИВ.Департамент_Здравоохранения"

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

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

Мы рассмотрели все основные шаги, которые могут быть использованы в сценарии и теперь переходим к "наполнению" этих шагов данными, используемыми при проверке и выполнении.

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

В сценарии возможно задавать параметры для шага с использованием переменных и примеров(таблиц).

Использование примеров и выражений

В различных сценариях используются данные, которые в последующем необходимо "использовать" в реализации конкретных шагов. Кроме того, одинаковые шаги сценария с РАЗЛИЧНЫМИ данными могут и должны быть сгруппированы в ОДИН шаг для выполнения, путем передачи необходимых параметров данному шагу.

Существует ДВА основных способа получения данных из сценария в шаге.

  1. Использование регулярных выражений с capture group.

  2. Использование cucumber выражений Cucumber Expressions

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

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

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

Отдельно отметим, что существует еще способ передачи аргументов в виде таблицы(см. далее).

Итак, у нас есть файл с расширением feature, в котором описаны различные сценарии тестов, часть из которых может содержать данные, используемые для выполнения и оценки результатов теста.

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

Определение шагов

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

# language: ru
@all @javascript
Функционал: Создание Классификатора и его заполнение

  Предыстория:                                                 # features/classifier.feature:5
    Дано Пользователь перешёл по ссылке в меню "Классификатор" # features/classifier.feature:6

  Сценарий: Создание классификатора                        # features/classifier.feature:7
    Если Пользователь вводит название классификатора "ОИВ" # features/classifier.feature:8
    И Нажимает "Создать Классификатор"                     # features/classifier.feature:9
    То Создается классификатор с названием "ОИВ"           # features/classifier.feature:10

...

3 scenarios (3 undefined)
21 steps (21 undefined)
0m0.317s

Для того, чтобы тест начал работать, необходимо определить шаги.

Как создавать шаги

Как правило, описание шагов расположено в каталоге features/step_definitions, который cucumber создает при установке. Но имя каталога не является обязательным и может быть любым. По умолчанию cucumber просматривает все файлы с расширением .rb внутри каталога features, в которых и ищет описание шагов.

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

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

Рекомендательный вывод cucumber для создания шагов.

You can implement step definitions for undefined steps with these snippets:

Дано('Пользователь перешёл по ссылке в меню {string}') do |string|
  pending # Write code here that turns the phrase above into concrete actions
end

Если('Пользователь вводит название классификатора {string}') do |string|
  pending # Write code here that turns the phrase above into concrete actions
end

Если('Нажимает {string}') do |string|
  pending # Write code here that turns the phrase above into concrete actions
end

То('Создается классификатор с названием {string}') do |string|
  pending # Write code here that turns the phrase above into concrete actions
end

Это готовый код для определения шагов, который надо "перевести" в действия.

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


Копируем содержимое вывода в файл с названием classifier.rb.

Про типы переменных и какие они бывают в cucumber чуть ниже.

Осталось заменить внутри блока pending реальным исполняемым кодом.

Итоги

  • сценарий транслируется cucumber в конкретные шаги

  • шаги должны быть описаны в исполняемом файле(файлах)

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

  • при наличии нескольких "подходящих" шагов будет выдана ошибка.

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

  • переменные в угловых скобках (при использовании примеров и шаблонов сценария) уже имеют собственные имена.

  • при наличии нескольких переменных(параметров) в строке они присваиваются последовательно в блоке do

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

Использование выражений и переменных в cucumber

Использование переменных

Cucumber Expressions

По приведенной ссылке можно найти подробное руководство, где описаны типы параметров и выражений и способы работы с ними.

Простые примеры.

Если вводим 2
И нажимаем на клавишу "х"
И вводим 2
И нажимаем на клавишу "="
То Результат равен 4

Транслируем строки теста в шаги.


Если вводим {int} do |first_digits|
<код, где мы получаем значение переменной first_digits равное 2 с типом "целое" >
...
end

Использование преобразований ParameterType

Возможно задавать трансформацию переменой в другой объект, используя ParameterType, например:


I have a {color} ball

В этом случае переменную color, которая может принимать в строке описания теста значения red|blue|yellow можно переопределить следующим образом

ParameterType(
  name:        'color',
  regexp:      /red|blue|yellow/,
  type:        Color,
  useForSnippets: false,
  transformer: ->(s) { Color.new(s) }
)

Более подробное описание Type Registry

Немного пояснений.

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

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

  • type - не обязательный параметр, определяет тип возвращаемого значения после преобразования.

  • useForSnippets - не обязательный параметр, определяет, использовать ли это преобразование для АВТОМАТИЧЕСКОЙ генерации шагов теста.

Подобные преобразования рекомендуется хранить в features/support/parameter_types.rb отдельно от тестов.

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

Использование типа данных table

Данные типа "таблицы" обозначаются разделителем | (вертикальная черта). Табличные данные могут иметь заголовок.

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

Пример:


  То Классификатор должен содержать уровни:
      |Терапевт       |
      |Хирург         |
      |Офтальмолог    |
# classifier.rb
То 'Классификатор должен содержать уровни' do |table|
  @classifier_branch = table.raw
  ...
end

В Cucumber определен объект для работы с таблицами - Cucumber::Core::Test::DataTable, методы которого можно использовать.


 Cucumber::MultilineArgument::DataTable.instance_methods(false)
[:headers, :location, :to_hash, :rows, :to_s, :create_cell_matrix, :to_step_definition_arg, :transpose, :hashes, :clear_cache!, :file, :symbolic_hashes, :raw, :symbolize_key, :match, :rows_hash, :describe_to, :verify_table_width, :column_names, :index, :each_cells_row, :cells_rows, :map_headers, :map_column, :append_to, :diff!, :ensure_table, :build_hashes, :convert_columns!, :cell_matrix, :convert_headers!, :cells_to_hash, :verify_column, :text?, :header_cell, :col_width, :columns, :to_json, :file=]

Метод raw, например, позволяет сохранить данные таблицы в переменной для дальнейшего использования (в виде массива). Метод diff! позволяет сравнивать табличные данные.

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

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


  Шаблон сценария: Сегодня пятница или нет?
    Дано Сегодня "<day>"
    Когда Я спрашиваю, пятница ли сегодня?
    То Мне должны ответить "<answer>"

  Примеры:
    | day                 |answer |
    | Пятница             | Да    |
    | Воскресенье         | Нет   |

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

Как все это связано между собой

В результате мы получили:

  1. Работающий cucumber запуска тестов.

  2. Файл с расширением .feature, где содержится наш первый сценарий.

  3. Файл с расширением .rb, в котором содержатся шаги для выполнения сценария с переменными, используемыми для заполнения данных, проверки и т.д.

  4. Возможность передавать и использовать данные из сценария в шаги теста.

Полезные мелочи

  • Остановка выполнения Capybara

Можно использовать ask метод для приостановки выполнения теста и получения ответа от оператора или истечения timeout, например для просмотра правильности заполнения полей.

#ask(question, timeout_seconds = 60) ⇒ Object
Pause the tests and ask the operator for input

When /^I enter "([^"])*" in the search field$/ do |query|
  visit('/search')
  fill_in('query', :with => query)
  ask('does that look right?')
end
  • Проверка(парсинг) строковых данных в тесте перед их сравнением или использованием способствует созданию более устойчивых тестов. (Matt, Cucumber)

  • Не используйте feature для BUGFIX, только для описания функциональности

  • Как тестировать модальные диалоги подтверждения при помощи Cucumber?

page.driver.browser.switch_to.alert.accept

page.driver.browser.switch_to.alert.dismiss

page.driver.browser.switch_to.alert.text

Полезные материалы

Большая часть материалов по тематике использования cucumber относится к его использованию совместно с java приложениями, что безусловно, полезно для ознакомления с точки зрения общего понимания, однако приведенными примерами для Rails воспользоваться как правило не удастся.

Ниже приведены ссылки на материалы по этой тематике.

Комментарии (0)