Несколько месяцев назад состоялся релиз Cucumber JVM 3.0.0. Новая версия призвана сделать работу с данным BDD фреймвоком более очевидной и гибкой. В данной статье я расскажу об изменениях и новых фичах, а также приведу примеры их использования.

Реализация Cucumber Expressions


В третьей версии Cucumber JVM стал доступен Cucumber Expressions — простой язык выражений для поиска подстрок в тексте. В отличие от регулярных выражений этот язык оптимизирован для удобочитаемости, что в контексте Cucumber'а имеет больший смысл. Если вам нужна гибкость, то по-прежнему можно воспользоваться регулярными выражениями.

Например, у нас есть следующая фича:

# language: ru
Функция: Передача аргументов различных типов

  Сценарий:
    * передадим в метод шага целое число 15
    * передадим в метод шага "текст"
    * передадим в метод шага hello

Для получения аргументов из неё можно воспользоваться следующим описанием шагов:

    @Допустим("передадим в метод шага целое число {int}")
    public void giveInt(Integer int1) {
        System.out.println(int1);
    }

    @Допустим("передадим в метод шага {string}")
    public void giveString(String string) {
        System.out.println(string);
    }

    @Допустим("передадим в метод шага {word}")
    public void giveWord(String string) {
        System.out.println(string);
    }

Как видно из примера Cucumber Expressions состоит из двух фигурных скобок с указанием типа передаваемого значения.

«Из коробки» доступна передача следующих типов:

  • {int}
  • {float}
  • {string}
  • {word}
  • {biginteger}
  • {bigdecimal}
  • {byte}
  • {short}
  • {long}
  • {double}

{string} соответствует строке в кавычках, а {word} — одному слову без кавычек (на момент написания статьи в выражение {word} можно передавать только слова, написанные латинскими буквами).

Можно создать свой тип данных, например мы хотим передавать из фичи объект класса LocalDate:

# language: ru
Функция: Передача пользовательского типа

  Сценарий:
    * передадим в метод дату 01.06.2018

    @Допустим("передадим в метод дату {localdate}")
    public void передадим_в_метод_дату(LocalDate localdate) {
        System.out.println(localdate.format(DateTimeFormatter.ofPattern("dd-MM-yyyy")));
    }

Для этого в пакете, указанном в glue, необходимо создать класс, реализующий интерфейс TypeRegistryConfigurer, и через него добавить свой тип данных в реестр:

public class TypeRegistryConfiguration implements TypeRegistryConfigurer {
    @Override
    public Locale locale() {
// требуется только для определения формата разделителя в float и double
        return new Locale("ru"); 
    }

    @Override
    public void configureTypeRegistry(TypeRegistry typeRegistry) {
// добавление в реестр определения необходимого типа
        typeRegistry.defineParameterType(new ParameterType<>(
// название параметра, используемое в определении шага:
                "localdate", 
// регулярка, для поиска необходимого значения в фиче:
                "[0-9]{2}.[0-9]{2}.[0-9]{4}",
// тип параметра: 
                LocalDate.class,
// функция, преобразующая входящую строку к нужному типу
                (Transformer<LocalDate>) s -> { 
                    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy");
                    return LocalDate.parse(s, formatter);
                }
        ));
    }
}

Класс, реализующий интерфейс TypeRegistryConfigurer, в проекте должен быть один, иначе будет выброшено исключение.

Также Cucumber Expressions позволяет в скобках указать необязательный текст:

    @Допустим("Hello, world(s)!")
    public void getHello() {
        System.out.println("Hello world!");
    }


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

Альтернативный текст задается через косую черту:

    @Допустим("основной/альтернативный текст")
    public void getAlternative() {
        System.out.println("Hello world!");
    }

# language: ru
Функция: Основной и альтернативный текст

  Сценарий:
    * альтернативный текст
    * основной текст

Экранировать {} и () можно обратной косой чертой.

В одном определении шага нельзя одновременно использовать Регулярные выражения и Cucumber-выражения.

Отказ от XStream


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

Обоснованием для отказа от XStreamConverters была плохая документация, отсутствие возможности использования сторонних мапперов объектов, а также отсутствие поддержки Java 9.
Аннотации Delimiter, Format, Transformer и другие аннотации из XStream больше не работают. Вместо них теперь необходимо использовать ParameterType или DataTableType.

DataTable


Тип данных DataTable также подвергся изменениям.

Как и в предыдущих версиях Cucumber 3 без проблем справляется с преобразованием DataTable с одним столбцом в List, с двумя столбцами в Map и т.п. Но это работает, только если преобразовывать данные в один из следующих типов: String, Integer, Float, Double, Byte, Short, Long, BigInteger или BigDecimal.

Если же вы хотите создать из DataTable объект какого-то другого класса, то вам, как и в случае пользовательского типа данных, необходимо написать свой преобразователь:

# language: ru
Функция: Передача пользовательского типа через DataTable

  Сценарий:
    Допустим у нас есть пользователи
      | Василий | Чапаев | 09.02.1887 |
      | Пётр      | Исаев   | 23.02.1890 |

User.java
import java.time.LocalDate;

public class User {
    private String firstName;
    private String lastName;
    private LocalDate birthDay;

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public LocalDate getBirthDay() {
        return birthDay;
    }

    public void setBirthDay(LocalDate birthDay) {
        this.birthDay = birthDay;
    }

    @Override
    public String toString() {
        return "User{" +
                "firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", birthDay=" + birthDay +
                '}';
    }
}


    @Допустим("у нас есть пользователи")
    public void у_нас_есть_пользователи(List<User> users) {
        System.out.println(users);
    }

Преобразователь добавляется в реестр таким же образом, как и в примере с пользовательским типом данных:

public class TypeRegistryConfiguration implements TypeRegistryConfigurer {
    @Override
    public Locale locale() {
        return new Locale("ru");
    }

    @Override
    public void configureTypeRegistry(TypeRegistry typeRegistry) {
// в этот раз в реестр добавляем DataTableType
        typeRegistry.defineDataTableType(new DataTableType(
                User.class,
                (TableRowTransformer<User>) list -> {
                    User user = new User();
                    user.setFirstName(list.get(0));
                    user.setLastName(list.get(1));
                    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy");
                    user.setBirthDay(LocalDate.parse(list.get(2), formatter));
                    return user;
                }
        ));
    }
}

Before и After Step Hooks


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

Step hooks работают по тем же правилам, что и хуки, относящиеся к уровню сценария:

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

# language: ru
Функция: Фикстуры

  @hooks
  Сценарий: вызов фикстур до и после сценария, а также перед и после каждого шага
    Дано Первый шаг
    Когда Второй шаг
    Тогда Третий шаг

  @only_scenario_hooks
  Сценарий: вызов фикстур только до и после сценария
    Дано Первый шаг
    Когда Второй шаг
    Тогда Третий шаг

  @only_step_hooks
  Сценарий: вызов фикстур только перед и после каждого шага
    Дано Первый шаг
    Когда Второй шаг
    Тогда Третий шаг

// секция not написана исключительно ради демонстрации
// выполнится только перед сценариями с тэгами hooks и only_scenario_hooks
    @Before(value = "(@hooks or @only_scenario_hooks) and not @only_step_hooks")
    public void before() {
        System.out.println("before scenario");
    }

// выполнится перед каждым шагом в сценарии с тэгом only_step_hooks
    @BeforeStep(value = "@only_step_hooks")
    public void beforeStep() {
        System.out.println("before step");
    }

// выполнится после каждого шага в сценарии с тэгом only_step_hooks
    @AfterStep(value = "not(@hooks or @only_scenario_hooks) and @only_step_hooks")
    public void afterStep() {
        System.out.println("after step");
    }

// выполнится только после сценариев с тэгами hooks и only_scenario_hooks
    @After(value = "@hooks or @only_scenario_hooks")
    public void after() {
        System.out.println("after scenario");
    }

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

Update 23.09.2018
Сегодня зарелизили Cucumber 4.0.0. Не буду описывать все изменения, скажу только, что теперь в Cucumber Expressions нормально поддерживается Русский язык, также, что написанный мной пример с {string} и {word} теперь работать не будет. Дело в том, что {word} теперь может содержать любой непробельный символ, что вызывает конфликт при поиске шага реализации:

# language: ru
Функция: Передача аргументов различных типов

  Сценарий:
    * передадим в метод шага "текст"
    * передадим в метод шага hello


    @Допустим("передадим в метод шага {string}")
    public void giveString(String string) {
        System.out.println(string);
    }

    @Допустим("передадим в метод шага {word}")
    public void giveWord(String string) {
        System.out.println(string);
    }


Ссылки:

> Примеры из статьи
> Моя статья по первой версии Cucumber JVM

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


  1. AlexMt
    22.09.2018 10:33

    А еще они отказались от поддержки closure, groovy, scala и еще ряда языков, во всяком случае силами core-команды, и ищут тех кто будет эти языки поддерживать.


    1. savkk Автор
      22.09.2018 20:21

      К теме статьи это не относится, но да, вы правы. Разработчики вынуждены были отказаться от поддержки других JVM-языков. На сколько я знаю, они стремятся поддерживать одинаковый функционал во всех реализациях Cucumber, но в связи с тем, что сейчас в core-jvm команде всего 3 человека и все они занимаются разработкой только в свободное время, а так же участвуют в других core-командах cucumber, поддерживать такое количество JVM-языков нереально.


      1. AlexMt
        22.09.2018 22:36
        +1

        Угу. Жалко вообще что новость практически незамеченной прошла, а ведь это важный релиз. В нём избавились от давнокода XStream-а, добавили произвольные словари (наконец-то!) и теперь можно создавать свои шаблоны, а это означает — прямой путь к генераторам всяких тестовых данных, формат которых теперь удобно задавать прямо в аргументе.

        У нас был на прошлом проекте наколхожен такой подход, к примеру, {TIMESTAMP-5h} и мы смотрели, если есть такое ключевое слово и фигурные скобки — всё передавалось в `@Transformer ` и он дальше решал, что с этим делать. Далее обычно шла длинная цепь проверок, «какому же трансформатому этот аргумент, чёрт возьми, принадлежит» :)) Теперь это в прошлом.

        Спасибо за статью! По поводу моего комментария хочу сказать что к теме статьи это и не относится напрямую, но у них в блоге новость вышла в преддверье релиза 3-го огурца, в котором они объясняли почему 3ий огурец для этих JVM-языков, скорее всего, не выйдет.