Разделы портала

Онлайн-тренинги

.
Использование таблиц данных в Cucumber-JVM для более читабельных спецификаций
13.07.2021 00:00

Автор: Баз Дейкстра (Bas Dijkstra)
Оригинал статьи
Перевод: Ольга Алифанова

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

В этой статье я хочу подробно разобрать фичу связок Java Cucumber, которые помогут вам писать читабельные спецификации: это использование таблиц данных.

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

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

Пример 1. Пары ключ-значение

Возьмем для первого примера ситуацию, когда вам нужно задать некое начальное состояние, состоящие из множества объектов ("штук"), которые можно смоделировать как простую пару ключ-значение. Например:

  • Валюта и ее код валюты (USD, EUR)
  • Аэропорты и их IATA-коды (LAX, JFK)
  • Имена сотрудников и их личные номера.

Или же, как в примере ниже, футбольные клубы и их домашние стадионы. Зачастую команды моделируют такие пары примерно так:

Сценарий: Перечисление стадионов футбольных клубов – подробный способ
   Если Ювентус играет дома на стадионе Альянс
   И Милан играет дома на стадионе Сан Сиро
   И Рома играет дома на стадионе Олимпико

Определение шагов для этих шагов:

@Given("^(.*) играет дома на стадионе (.*)$")
public void club_play_their_home_games_at_stadium(String club, String stadium) {
     System.out.printf("%s играет дома на стадионе %s%n", club, stadium);
}

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

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

While this works, technically, I believe there are two inherent problems with this example. First, it is tedious to read because of the repeated text. Second, we’re using three steps (a Given and two Ands) to specify a single initial state, which to me feels counterintuitive. As a whole, this kind of specification reminds me a little too much of this iconic movie scene..

There’s a much better way to include the same data in your specification, and that is by using a data table. The same specification can be rewritten as

Сценарий: Перечисление стадионов футбольных клубов – понятный способ
    Если есть следующие клубы и их стадионы
        | Ювентус | Альянс |
        | Милан | Сан Сиро|
        | Рома  | Олимпико |

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

Определение шагов может выглядеть так:

@Given("следующие клубы и их стадионы")
public void the_following_clubs_and_their_stadiums(Map<String, String> stadiums) {
    stadiums.forEach((club, stadium) ->
         System.out.printf("%s играет дома на стадионе %s%n", club, stadium)
    );
}

Как видите, Cucumber автоматически конвертирует таблицу данных, переданную в качестве аргумента на шаге @Given, в Map – по сути это коллекция пар ключ-значение. Затем вы можете переходить по парам при помощи forEach() и обрабатывать каждую запись так, как это необходимо для вашего приемочного теста.

Запуск этого примера выдаст следующий результат:

Ювентус играет дома на стадионе Альянс
Милан играет дома на стадионе Сан Сиро
Рома играет дома на стадионе Олимпико

Пример 2: многоколонные таблицы

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

  • Адреса (комбинация названия улицы, номера дома, индекса, города…)
  • Полетная информация (комбинация номера рейса, названия авиакомпании, аэропорта вылета, аэропорта прилета…)
  • Банковские переводы (комбинация даты, количества, исходного счета, счета назначения…)

Или, как в моем примере, это данные конкретных игроков футбольного клуба. Эти данные можно задать так:

Сценарий: перечисление футбольных игроков – подробный способ.
    Если Криштиану Роналдо , Португалия, родился 05/02/1985, играет за Ювентус с сезона 2018/2019
    И Маттейс де Лигт, Нидерланды, родился 12/08/1999, играет за Ювентус с сезона 2019/2020
    И Джорджо Кьеллини, Италия, родился 14/08/1984, играет за Ювентус с сезона 2005/2006

Шаги можно внедрить, используя следующий метод:

@Given("^(.*) of (.*), born on (.*), plays for Juventus since the (.*) season$")
public void name_of_country_born_on_date_plays_for_club_since_the_years_season(String name, String nationality, String dateOfBirth, String firstSeason) {
    System.out.printf("%s of %s, born on %s, plays for Juventus since the %s season%n", name, nationality, dateOfBirth, firstSeason);
}

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

То же самое состояние можно смоделировать при помощи таблицы данных:

Если в Ювентус есть игроки:

Name

Nationality

dateOfBirth

atJuventusSince

Cristiano Ronaldo

Portugal

05-02-1985

2018/2019

Matthijs de Ligt

the Netherlands

12-08-1999

2019/2020

Giorgio Chiellini

Italy

14-08-1984

2005/2006

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

@Given("the following Juventus players")
public void the_following_juventus_players(List<Map<String, String>> players) {
    for(Map<String, String> player : players) {
        System.out.printf(
            "%s of %s, born on %s, plays for Juventus since the %s season%n",
            player.get("name"),
            player.get("nationality"),
           player.get("dateOfBirth"),
           player.get("atJuventusSince")
        );
    }
}

Cucumber автоматически конвертирует структуру таблицы в аргумент типа List<Map<String, String>>, или, говоря по-русски, в список карт, где каждая Карта – это элемент данных (в данном случае игрок), а каждый игрок имеет отдельные атрибуты, заданные парами ключ-значение.

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

Запуск примера даст следующий результат:

Cristiano Ronaldo of Portugal, born on 05-02-1985, plays for Juventus since the 2018/2019 season
Matthijs de Ligt of the Netherlands, born on 12-08-1999, plays for Juventus since the 2019/2020 season
Giorgio Chiellini of Italy, born on 14-08-1984, plays for Juventus since the 2005/2006 season

Пример 3: Таблицы с заголовками колонок и строк

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

  • Доступность службы доставки в зависимости от дня недели и времени суток
  • Поездные тарифы в зависимости от класса места и возрастной группы
  • Распределение золотых, серебряных и бронзовых медалей между странами, соревнующимися в Олимпийских играх

Или же, как в примере ниже, это финальные очки футбольных матчей.

Сценарий: перечисление истории результатов футбольных матчей – подробный способ
    Если финальный результат Дерби Италии 17/01/2021 был Интернационале – 2, Ювентус – 0
    И финальный результат Дерби Италии 08/03/2020 был Интернационале 0, Ювентус 2
    И финальный результат Дерби Италии 06/10/2019 был Интернационале 1, Ювентус 2

Внедрение:

@Given("^the final score of the Derby d'Italia played on (.*) was Internazionale (\\d+), Juventus (\\d+)$")
public void the_final_score_of_the_derby_dItalia_played_on_date_was_Internazionale_score_Juventus_score(String date, int interGoals, int juveGoals) {
    System.out.printf("The final score of the Derby d'Italia played on %s was Internazionale %d, Juventus %d", date, interGoals, juveGoals);
}

И снова много повторов в спецификации, и это скучно читать, вы это уже знаете. К счастью, и с этим можно справиться:

Сценарий: перечисление истории результатов футбольных матчей – понятный способ

Если история результатов Дерби Италии такова, что:


Internazionale

Juventus

17/01/2021

2

0

08/03/2020

0

2

16/10/2019

1

2

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

@Given("the following historic Derby d'Italia results")
public void the_following_historic_derby_dItalia_results(Map<String, Map<String, Integer>> results) {
    results.forEach((date, scores) ->
        System.out.printf(
             "The final score of the Derby d'Italia played on %s was Internazionale %d, Juventus %d%n",
             date,
             scores.get("Internazionale"),
             scores.get("Juventus")
        )
    );
}

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

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

При запуске примера вы получите следующее:

The final score of the Derby d'Italia played on 17-01-2021 was Internazionale 2, Juventus 0
The final score of the Derby d'Italia played on 08-03-2020 was Internazionale 0, Juventus 2
The final score of the Derby d'Italia played on 06-10-2019 was Internazionale 1, Juventus 2

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

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

Весь приведенный в статье код можно найти на GitHub.

Обсудить в форуме