Использование таблиц данных в Cucumber-JVM для более читабельных спецификаций |
13.07.2021 00:00 | ||||||||||||||||||||||||||||
Автор: Баз Дейкстра (Bas Dijkstra) Если вы когда-либо работали в команде, практикующей BDD и использующей Cucumber или SpecFlow для создания исполняемых спецификаций, то вы знаете, как тяжело писать читабельные сценарии. Очень, очень тяжело! В этой статье я хочу подробно разобрать фичу связок Java Cucumber, которые помогут вам писать читабельные спецификации: это использование таблиц данных. Таблицы данных – это таблицы, которые можно передавать в отдельный шаг в качестве аргумента. Данные в этой таблице затем будут обработаны согласно определению шага. Таблицы данных не надо путать с таблицами примеров – таблицы примеров содержат примеры для сценариев целиком и используются в описаниях сценариев. Таблицы данных позволяют использовать более сложные структуры данных в качестве аргумента для шага. Давайте рассмотрим ряд примеров с применением различных форм таблиц данных, а также сравним, как это будет выглядеть, если те же самые данные определять в текстовом формате. Пример 1. Пары ключ-значение Возьмем для первого примера ситуацию, когда вам нужно задать некое начальное состояние, состоящие из множества объектов ("штук"), которые можно смоделировать как простую пару ключ-значение. Например:
Или же, как в примере ниже, футбольные клубы и их домашние стадионы. Зачастую команды моделируют такие пары примерно так: Сценарий: Перечисление стадионов футбольных клубов – подробный способ Определение шагов для этих шагов: @Given("^(.*) играет дома на стадионе (.*)$") Технически это работает, но я вижу тут две внутренние проблемы. Во-первых, это сложно читать из-за повторяющегося текста. Во-вторых, мы используем три шага (один Если и два И) для определения единичного исходного состояния – это контринтуитивно. В целом такая спецификация очень напоминает мне бессмертную сцену из кино. Есть куда лучший способ вставить те же самые данные в спецификацию – это использование таблиц данных. Спецификацию можно переписать так: 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("следующие клубы и их стадионы") Как видите, Cucumber автоматически конвертирует таблицу данных, переданную в качестве аргумента на шаге @Given, в Map – по сути это коллекция пар ключ-значение. Затем вы можете переходить по парам при помощи forEach() и обрабатывать каждую запись так, как это необходимо для вашего приемочного теста. Запуск этого примера выдаст следующий результат: Ювентус играет дома на стадионе Альянс Пример 2: многоколонные таблицы Как насчет ситуаций, когда нужно смоделировать данные с большим количеством атрибутов, и нельзя записать их как пары ключ-значение? Например, это:
Или, как в моем примере, это данные конкретных игроков футбольного клуба. Эти данные можно задать так: Сценарий: перечисление футбольных игроков – подробный способ. Шаги можно внедрить, используя следующий метод: @Given("^(.*) of (.*), born on (.*), plays for Juventus since the (.*) season$") Этот пример страдает от тех же бед, что и предыдущий. Тут много повторов, спецификацию скучно читать, и нам нужно несколько шагов для создания начального состояния. Улучшим это! То же самое состояние можно смоделировать при помощи таблицы данных: Если в Ювентус есть игроки:
Заметьте, что тут я включаю заголовки таблицы, потому что так гораздо яснее, какая колонка какому атрибуту игрока соответствует. Использование заголовков крайне полезно для кода соответствующего определения шагов: @Given("the following Juventus players") Cucumber автоматически конвертирует структуру таблицы в аргумент типа List<Map<String, String>>, или, говоря по-русски, в список карт, где каждая Карта – это элемент данных (в данном случае игрок), а каждый игрок имеет отдельные атрибуты, заданные парами ключ-значение. Для перехода по списку мы снова используем цикл forEach(), как можно видеть в примере кода выше. Каждое значение свойства получается с использованием соответствующего ключа – это заголовок соответствующей колонки в таблице нашей спецификации. Запуск примера даст следующий результат: Cristiano Ronaldo of Portugal, born on 05-02-1985, plays for Juventus since the 2018/2019 season Пример 3: Таблицы с заголовками колонок и строк И, напоследок, рассмотрим еще более сложную структуру данных – таблицу, у которой есть заголовки как столбцов, так и строк. Примеры использования такой таблицы:
Или же, как в примере ниже, это финальные очки футбольных матчей. Сценарий: перечисление истории результатов футбольных матчей – подробный способ Внедрение: @Given("^the final score of the Derby d'Italia played on (.*) was Internazionale (\\d+), Juventus (\\d+)$") И снова много повторов в спецификации, и это скучно читать, вы это уже знаете. К счастью, и с этим можно справиться: Сценарий: перечисление истории результатов футбольных матчей – понятный способ Если история результатов Дерби Италии такова, что:
Как можно видеть, состояние можно смоделировать с использованием таблицы, у которой есть заголовки как строк, так и столбцов. Cucumber снова напрямую трансформирует таблицу в подходящую структуру данных: @Given("the following historic Derby d'Italia results") Дата преобразуется в карту, где ключи – это заголовки строк (даты матчей), а значения – тоже карты, у которых ключи – это заголовки колонок (названия клубов), а значения – количество набранных голов. В этом примере мы тоже переходим по внешней карте для обработки каждого матча с использованием forEach(), и мы пользуемся заголовками колонок для получения значений (счета каждой команды) из внутренней карты, используя get() При запуске примера вы получите следующее: The final score of the Derby d'Italia played on 17-01-2021 was Internazionale 2, Juventus 0 В этой статье мы рассмотрели несколько примеров использования таблиц данных для более эффективного описания сложных данных в .feature-файлах Cucumber. Мы увидели, как Cucumber автоматически трансформирует эти таблицы в специфические структуры данных, и как переходить по ним, чтобы эффективно обработать такие данные. По сути мы убрали итерации из спецификаций, сделав их читабельнее, и перенесли их в определения шагов. В следующей статье я глубже разберусь в трансформерах таблиц данных, нужных для еще более эффективного управления сложными структурами. Весь приведенный в статье код можно найти на GitHub. |