- PVSM.RU - https://www.pvsm.ru -
К сожалению, нет магической формулы для разработки высококачественного программного обеспечения, но очевидно, что тестирование улучшает его качество, а автоматизация тестирования улучшает качество самого тестирования.
В данной статье мы рассмотрим один из самых популярных фреймворков для автоматизации тестирования с использованием BDD-подхода – Cucumber. Также посмотрим, как он работает и какие средства предоставляет.
Первоначально Cucumber был разработан Ruby-сообществом, но со временем был адаптирован и для других популярных языков программирования. В данной статье рассмотрим работу Cucumber на языке Java.
BDD тесты – это простой текст, на человеческом языке, написанный в форме истории (сценария), описывающей некоторое поведение.
В Cucumber для написания тестов используется Gherkin-нотация, которая определяет структуру теста и набор ключевых слов. Тест записывается в файл с расширением *.feature и может содержать как один, так и более сценариев.
Рассмотрим пример теста на русском языке с использованием Gherkin:
# language: ru
@all
Функция: Аутентификация банковской карты
Банкомат должен спросить у пользователя PIN-код банковской карты
Банкомат должен выдать предупреждение, если пользователь ввел неправильный PIN-код
Аутентификация успешна, если пользователь ввел правильный PIN-код
Предыстория:
Допустим пользователь вставляет в банкомат банковскую карту
И банкомат выдает сообщение о необходимости ввода PIN-кода
@correct
Сценарий: Успешная аутентификация
Если пользователь вводит корректный PIN-код
То банкомат отображает меню и количество доступных денег на счету
@fail
Сценарий: Некорректная аутентификация
Если пользователь вводит некорректный PIN-код
То банкомат выдает сообщение, что введённый PIN-код неверный
Как видно из примера, сценарии описаны на простом нетехническом языке, благодаря чему, понимать и писать их может любой участник проекта.
Обратите внимание на структуру сценария:
1. Получить начальное состояние системы;
2. Что-то сделать;
3. Получить новое состояние системы.
В примере жирным выделены ключевые слова. Ниже представлен полный список ключевых слов на русском языке:
Ключевые слова, перечисленные в пунктах 1-5, используются для описания шагов сценария, Cucumber их технически не различает. Вместо них можно использовать символ *, но делать так не рекомендуется. У этих слов есть определенная цель, и они были выбраны именно для неё.
Список зарезервированных символов:
# – обозначает комментарии;
@ – тэгирует сценарии или функционал;
| – разделяет данные в табличном формате;
""" – обрамляет многострочные данные.
Сценарий начинается со строки # language: ru. Эта строчка указывает Cucumber, что в сценарии используется русский язык. Если её не указать, фреймворк, встретив в сценарии русский текст, выбросит исключение LexingError и тест не запустится. По умолчанию используется английский язык.
Cucumber-проект состоит из двух частей – это текстовые файлы с описанием сценариев (*.feature) и файлы с реализацией шагов на языке программирования (в нашем случае — файлы *.java).
Для создания проекта будем использовать систему автоматизации сборки проектов Apache Maven.
Первым делом добавим cucumber в зависимости Maven:
<dependency>
<groupId>info.cukes</groupId>
<artifactId>cucumber-java</artifactId>
<version>1.2.4</version>
</dependency>
Для запуска тестов будем использовать JUnit (возможен запуск через TestNG), для этого добавим еще две зависимости:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>info.cukes</groupId>
<artifactId>cucumber-junit</artifactId>
<version>1.2.4</version>
</dependency>
Библиотека cucumber-junit содержит класс cucumber.api.junit.Cucumber, который позволяет запускать тесты, используя JUnit аннотацию RunWith. Класс, указанный в этой аннотации, определяет каким образом запускать тесты.
Создадим класс, который будет являться точкой входа для наших тестов.
import cucumber.api.CucumberOptions;
import cucumber.api.SnippetType;
import cucumber.api.junit.Cucumber;
import org.junit.runner.RunWith;
@RunWith(Cucumber.class)
@CucumberOptions(
features = "src/test/features",
glue = "ru.savkk.test",
tags = "@all",
dryRun = false,
strict = false,
snippets = SnippetType.UNDERSCORE,
// name = "^Успешное|Успешная.*"
)
public class RunnerTest {
}
Обратите внимание, название класса должно иметь окончание Test, иначе тесты не будут запускаться.
Рассмотрим опции Cucumber:
Для фильтрации запускаемых тестов нельзя одновременно использовать опции tags и name.
Создание «фичи»
В папке src/test/features создадим файл с описание тестируемого функционала. Опишем два простых сценария снятия денег со счета — успешный и провальный.
# language: ru
@withdrawal
Функция: Снятие денег со счета
@success
Сценарий: Успешное снятие денег со счета
Дано на счете пользователя имеется 120000 рублей
Когда пользователь снимает со счета 20000 рублей
Тогда на счете пользователя имеется 100000 рублей
@fail
Сценарий: Снятие денег со счета - недостаточно денег
Дано на счете пользователя имеется 100 рублей
Когда пользователь снимает со счета 120 рублей
Тогда появляется предупреждение "На счете недостаточно денег"
Запускаем
Попробуем запустить RunnerTest со следующими настройками:
@RunWith(Cucumber.class)
@CucumberOptions(
features = "src/test/features",
glue = "ru.savkk.test",
tags = "@withdrawal",
snippets = SnippetType.CAMELCASE
)
public class RunnerTest {
}
В консоль появился результат прохождения теста:
Undefined scenarios:
test.feature:6 # Сценарий: Успешное снятие денег со счета
test.feature:12 # Сценарий: Снятие денег со счета - недостаточно денег
2 Scenarios (2 undefined)
6 Steps (6 undefined)
0m0,000s
You can implement missing steps with the snippets below:
@Дано("^на счете пользователя имеется (\d+) рублей$")
public void наСчетеПользователяИмеетсяРублей(int arg1) throws Throwable {
// Write code here that turns the phrase above into concrete actions
throw new PendingException();
}
@Когда("^пользователь снимает со счета (\d+) рублей$")
public void пользовательСнимаетСоСчетаРублей(int arg1) throws Throwable {
// Write code here that turns the phrase above into concrete actions
throw new PendingException();
}
@Тогда("^появляется предупреждение "([^"]*)"$")
public void появляетсяПредупреждение(String arg1) throws Throwable {
// Write code here that turns the phrase above into concrete actions
throw new PendingException();
}
Cucumber не нашел реализацию шагов и предложил свои шаблоны для разработки.
Создадим класс MyStepdefs в пакете ru.savkk.test и перенесем в него методы, предложенные фреймворком:
import cucumber.api.PendingException;
import cucumber.api.java.ru.*;
public class MyStepdefs {
@Дано("^на счете пользователя имеется (\d+) рублей$")
public void наСчетеПользователяИмеетсяРублей(int arg1) throws Throwable {
// Write code here that turns the phrase above into concrete actions
throw new PendingException();
}
@Когда("^пользователь снимает со счета (\d+) рублей$")
public void пользовательСнимаетСоСчетаРублей(int arg1) throws Throwable {
// Write code here that turns the phrase above into concrete actions
throw new PendingException();
}
@Тогда("^появляется предупреждение "([^"]*)"$")
public void появляетсяПредупреждение(String arg1) throws Throwable {
// Write code here that turns the phrase above into concrete actions
throw new PendingException();
}
}
При запуске теста Cucumber проходит по сценарию шаг за шагом. Взяв шаг, он отделяет ключевое слово от описания шага и пытается найти в Java-классах пакета указанного в опции glue аннотацию с регулярным выражением, подходящим описанию. Найдя совпадение, фреймворк вызывает метод с найденной аннотацией. Если несколько регулярных выражений удовлетворяют описанию шага, фреймворк выбрасывает ошибку.
Как было сказано выше, для Cucumber технически нет отличия в ключевых словах, описывающих шаги, это верно и для аннотации, например:
@Когда("^пользователь снимает со счета (\d+) рублей$")
и
@Тогда("^пользователь снимает со счета (\d+) рублей$")
для фреймворка являются одинаковыми.
То, что в регулярных выражениях записано в скобках передается в метод в виде аргумента. Фреймворк самостоятельно определяет, что необходимо передавать из сценария в метод в виде аргумента. Это числа — (\d+). И текст, экранированный в кавычки — "([^"]*)". Это самые распространённые из передаваемых аргументов.
Ниже в таблице представлены элементы, используемые в регулярных выражениях:
регулярных выражениях:
Выражение
Описание
Соответствие
.
Один любой символ (за исключение
переноса строки)
Ф
2
j
.*
0 или больше любых символов
(за исключением переноса строки)
Abracadabra
789-160-87
,
.+
Один или больше любых символов
(за исключением переноса строки)
Все, что относилось к
предыдущему, за исключением пустой
строки.
.{2}
Любые два символа (за
исключением переноса строки)
Фф
22
$х
JJ
.{1,3}
От одного до трех любых
символов (за исключением переноса
строки)
Жжж
Уу
!
^
Якорь начала строки
^aaa соответствует aaa
^aaa соответствует aaabbb
^aaa не соответствует bbbaaa
$
Якорь конца строки
aaa$ соответствует aaa
aaa$ не соответствует aaabbb
aaa$ соответствует bbbaaa
d*
[0-9]*
Любое число (или ничего)
12321
5323
d+
[0-9]+
Любое число
Все, что относилось к
предыдущему, за исключением пустой
строки.
w*
Любая буква, цифра или нижнее
подчеркивание (или ничего)
_we
_1ee
Gfd4
s
Пробел, табуляция или перенос
строки
t, r
или n
"[^"]*"
Любой символ (или ничего) в
кавычках
"aaa"
""
"3213dsa"
?
Делает символ или группу
символов необязательными
abc?
соответствует ab
или abc, но не b или bc
|
Логическое ИЛИ
aaa|bbb
соответствует aaa
или bbb, но не aaabbb
()
Группа. В Cucumber
группа передается в определение шага
в виде аргумента.
(d+) рублей соответствует 10 рублей,
при этом 10 передается в метод шага в
виде аргумента
(?: )
Не передаваемая группа.
Cucumber не воспринимает
группу как аргумент.
(d+) (?:рублей|рубля) соответствует 3
рубля, при этом 3 передается в метод,
а «рубля» - нет.
Часто возникает ситуация, когда из сценария в метод необходимо передать набор однотипных данных – коллекций. Для подобной задачи в Cucumber есть несколько решений:
Дано в меню доступны пункты Файл, Редактировать, О программе
@Дано("^в меню доступны пункты (.*)$")
public void вМенюДоступныПункты(List<String> arg) {
// что-то сделать
}
Для замены разделителя, можно воспользоваться аннотацией Delimiter:
Дано в меню доступны пункты Файл и Редактировать и О программе
@Дано("^в меню доступны пункты (.+)$")
public void вМенюДоступныПункты(@Delimiter(" и ") List<String> arg) {
// что-то сделать
}
Дано в меню доступны пункты
| Файл |
| Редактировать |
| О программе |
@Дано("^в меню доступны пункты$")
public void вМенюДоступныПункты(List<String> arg) {
// что-то сделать
}
Дано в меню доступны пункты
| Файл | true |
| Редактировать | false |
| О программе | true |
public void вМенюДоступныПункты(Map<String, Boolean> arg) {
// что-то сделать
}
Дано в меню доступны пункты
| Файл | true | 5 |
| Редактировать | false | 8 |
| О программе | true | 2 |
@Дано("^в меню доступны пункты$")
public void вМенюДоступныПункты(DataTable arg) {
// что-то сделать
}
DataTable – это класс, который эмулирует табличное представление данных. Для доступа к данным в нем имеется большое количество методов. Рассмотрим некоторые из них:
public <K,V> List<Map<K,V>> asMaps(Class<K> keyType,Class<V> valueType)
Конвертирует таблицу в список ассоциативных массивов. Первая строка таблицы используется для именования ключей, остальные как значения:
Дано в меню доступны пункты
| Название | Доступен | Количество подменю |
| Файл | true | 5 |
| Редактировать | false | 8 |
| О программе | true | 2 |
@Дано("^в меню доступны пункты$")
public void вМенюДоступныПункты(DataTable arg) {
List<Map<String, String>> table = arg.asMaps(String.class, String.class);
System.out.println(table.get(0).get("Название"));
System.out.println(table.get(1).get("Название"));
System.out.println(table.get(2).get("Название"));
}
Данный пример выведет на консоль:
Файл
Редактировать
О программе
public <T> List<List<T>> asLists(Class<T> itemType)
Метод преобразует таблицу в список списков:
Дано в меню доступны пункты
| Файл | true | 5 |
| Редактировать | false | 8 |
| О программе | true | 2 |
@Дано("^в меню доступны пункты$")
public void вМенюДоступныПункты(DataTable arg) {
List<List<String>> table = arg.asLists(String.class);
System.out.print(table.get(0).get(0) + " ");
System.out.print(table.get(0).get(1) + " ");
System.out.println(table.get(0).get(2) + " ");
System.out.print(table.get(1).get(0) + " ");
System.out.print(table.get(1).get(1) + " ");
System.out.println(table.get(1).get(2) + " ");
}
На консоль будет выведено:
Файл true 5
Редактировать false 8
public List<List<String>> cells(int firstRow)
Этот метод делает то же, что и предыдущий метод, за исключением того, что нельзя определить какого типа данные находятся в таблице, всегда возвращает список строк – List. В качестве аргумента метод принимает номер первой строки:
Дано в меню доступны пункты
| Файл | true | 5 |
| Редактировать | false | 8 |
| О программе | true | 2 |
@Дано("^в меню доступны пункты$")
public void вМенюДоступныПункты(DataTable arg) {
List<List<String>> table = arg.cells(1);
System.out.print(table.get(0).get(0) + " ");
System.out.print(table.get(0).get(1) + " ");
System.out.println(table.get(0).get(2) + " ");
System.out.print(table.get(1).get(0) + " ");
System.out.print(table.get(1).get(1) + " ");
System.out.println(table.get(1).get(2) + " ");
}
Метод выведет на консоль:
Редактировать false 8
О программе true 2
Создадим для примера класс Menu:
public class Menu {
private String title;
private boolean isAvailable;
private int subMenuCount;
public String getTitle() {
return title;
}
public boolean getAvailable() {
return isAvailable;
}
public int getSubMenuCount() {
return subMenuCount;
}
}
Для первого способа шаг в сценарии запишем в следующем виде:
Дано в меню доступны пункты
| title | isAvailable | subMenuCount |
| Файл | true | 5 |
| Редактировать | false | 8 |
| О программе | true | 2 |
Реализация:
@Дано("^в меню доступны пункты$")
public void вМенюДоступныПункты(List<Menu> arg) {
for (int i = 0; i < arg.size(); i++) {
System.out.print(arg.get(i).getTitle() + " ");
System.out.print(Boolean.toString(arg.get(i).getAvailable()) + " ");
System.out.println(Integer.toString(arg.get(i).getSubMenuCount()));
}
}
Вывод в консоль:
Файл true 5
Редактировать false 8
О программе true 2
Фреймворк создает связанный список объектов из таблицы с тремя колонками. В первой строке таблицы должны быть указаны наименования полей класса, создаваемого объекта. Если какое-то поле не указать, оно не будет инициализировано.
Для второго способа приведем шаг сценария к следующему виду:
Дано в меню доступны пункты
| title | Файл | Редактировать | О программе |
| isAvailable | true | false | true |
| subMenuCount | 5 | 8 | 2 |
А в аргументе описания шага используем аннотацию @Transpose.
@Дано("^в меню доступны пункты$")
public void вМенюДоступныПункты(@Transpose List<Menu> arg) {
// что-то сделать
}
Cucumber, как и в предыдущем примере, создаст связанный список объектов, но, в данном случае, наименования полей записывается в первой колонке таблицы.
Для передачи многострочных данных в аргумент метода, их необходимо экранировать тремя двойными кавычками:
Тогда отображается форма с текстом
"""
На ваш номер телефона был выслан одноразовый пароль.
Для подтверждения платежа необходимо ввести полученный
одноразовый пароль.
"""
Данные в метод приходят в виде объекта класса String:
@Тогда("^отображается форма с текстом$")
public void отображаетсяФормаСТекстом(String expectedText) {
// что-то сделать
}
Фреймворк самостоятельно приводит данные из сценария к типу данных, указанному в аргументе метода. Если это невозможно, то выбрасывает исключение ConversionException. Это справедливо и для классов Date и Calendar. Рассмотрим пример:
Дано дата создания документа 04.05.2017
@Дано("^дата создания документа (.+)$")
public void датаСозданияДокумента (Date arg) {
// что-то сделать
}
Все прекрасно сработало, Cucumber преобразовал 04.05.2017 в объект класса Date со значением «Thu May 04 00:00:00 EET 2017».
Рассмотрим еще один пример:
Дано дата создания документа 04-05-2017
@Дано("^дата создания документа (.+)$")
public void датаСозданияДокумента (Date arg) {
// что-то сделать
}
Дойдя до этого шага, Cucumber выбросил исключение:
cucumber.deps.com.thoughtworks.xstream.converters.ConversionException: Couldn't convert "04-05-2017" to an instance of: [class java.util.Date]
Почему первый пример сработал, а второй нет?
Дело в том, что в Cucumber встроена поддержка форматов дат чувствительных к текущей локали. Если необходимо записать дату в формате, отличающемся от формата текущей локали, нужно использовать аннотацию Format:
Дано дата создания документа 04-05-2017
@Дано("^дата создания документа (.+)$")
public void датаСозданияДокумента (@Format("dd-MM-yyyy") Date arg) {
// что-то сделать
}
Бывают случаи, когда необходимо запустить тест несколько раз с различным набором данных, в таких случая на помощь приходит конструкция «Структура сценария»:
# language: ru
@withdrawal
Функция: Снятие денег со счета
@success
Структура сценария: Успешное снятие денег со счета
Дано на счете пользователя имеется <изначально> рублей
Когда пользователь снимает со счета <снято> рублей
Тогда на счете пользователя имеется <осталось> рублей
Примеры:
| изначально | снято | осталось |
| 10000 | 1 | 9999 |
| 9999 | 9999 | 0 |
Суть данной конструкции заключается в том, что в места, обозначенные символами <>, вставляются данные из таблицы Примеры. Тест будет запускаться поочередно для каждой строки из данной таблицы. Названия колонок должно совпадать с названием мест вставки данных.
Cucumber поддерживает хуки (hooks) – методы, запускаемые до или после сценария. Для их обозначения используется аннотация Before и After. Класс с хуками должен находиться в пакете, указанном в опциях фреймворка. Пример класса с хуками:
import cucumber.api.java.After;
import cucumber.api.java.Before;
public class Hooks {
@Before
public void prepareData() {
//подготовить данные
}
@After
public void clearData() {
//очистить данные
}
}
Метод c аннотацией Before будет запускаться перед каждым сценарием, After – после.
Хукам можно задать порядок, в котором они будут выполняться. Для этого необходимо в аннотации указать параметр order. По умолчанию значение order равно 10000, чем меньше это значение, тем раньше выполнится метод:
@Before(order = 10)
public void connectToServer() {
//подключиться к серверу
}
@Before(order = 20)
public void prepareData() {
//подготовить данные
}
В данном примере первым выполнится метод connectToServer(), затем prepareData().
В параметре value можно указать тэги сценариев, для которых будут отрабатывать хуки. Символ ~ означает «за исключением». Пример:
<source lang="java">@Before(value = "@correct", order = 30)
public void connectToServer() {
//сделай что-нибудь
}
@Before(value = "~@fail", order = 20)
public void prepareData() {
//сделай что-нибудь
}
Метод connectToServer будет выполнен для всех сценариев с тэгом correct [1], метод prepareData для всех сценариев за исключением сценариев с тэгом fail [2].
Если в определении метода-хука в аргументе указать объект класса Scenario, то в данном методе можно будет узнать много полезной информации о запущенном сценарии, например:
@After
public void getScenarioInfo(Scenario scenario) {
System.out.println(scenario.getId());
System.out.println(scenario.getName());
System.out.println(scenario.getStatus());
System.out.println(scenario.isFailed());
System.out.println(scenario.getSourceTagNames());
}
Для сценария:
# language: ru
@all
Функция: Аутентификация банковской карты
Банкомат должен спросить у пользователя PIN-код банковской карты
Банкомат должен выдать предупреждение если пользователь ввел неправильный PIN-код
Аутентификация успешна если пользователь ввел правильный PIN-код
Предыстория:
Допустим пользователь вставляет в банкомат банковскую карту
И банкомат выдает сообщение о необходимости ввода PIN-кода
@correct
Сценарий: Успешная аутентификация
Если пользователь вводит корректный PIN-код
То банкомат отображает меню и количество доступных денег на счету
Выведет в консоль:
аутентификация-банковской-карты;успешная-аутентификация
Успешная аутентификация
passed
false
[@correct, @all]
Cucumber — очень мощный и гибкий фреймворк, который можно использовать в связке со многими другими популярными инструментами. Например, с Selenium – фреймворком для автоматизации веб-приложений, или Yandex.Allure – библиотекой, позволяющей создавать удобные отчеты.
Всем удачи в автоматизации.
Автор: savkk
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/260043
Ссылки в тексте:
[1] correct: https://habrahabr.ru/users/correct/
[2] fail: https://habrahabr.ru/users/fail/
[3] Источник: https://habrahabr.ru/post/332754/?utm_source=habrahabr&utm_medium=rss&utm_campaign=sandbox
Нажмите здесь для печати.