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

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

.
Использование типов таблиц данных в Cucumber-JVM
04.07.2022 00:00

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

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

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

Я большой поклонник SpecFlow, BDD-фреймворка для .NET. Одна из наиболее симпатичных мне функций SpecFlow – это SpecFlow.Assist helpers, позволяющие быстро трансформировать таблицы из спецификаций в списки экземпляров C#-объектов, а также сравнивать списки объектов с таблицами – и все это путем одного вызова метода SpecFlow.Assist helper.

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

Наша спецификация Gherkin и определение Java-объекта

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

Scenario: Listing book details
     Given the following books
        | title                                   | author               | yearOfPublishing |
        | To kill a mockingbird           | Harper Lee         | 1960               |
        | The catcher in the rye         | J.D. Salinger        | 1951               |
        | The great Gatsby                | F. Scott Fitzgerald | 1925               |

Нам также нужно определить Java-объект, представляющий книгу, который мы назовем Book:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Book {
 
    private String title;
    private String author;
    private int yearOfPublishing;
}

Заметьте, что я использую Lombok-аннотации, чтобы автоматически генерировать получатели и установщики, а также конструкторы без аргументов и со всеми аргументами. Сильно бережет буквы. Если вы не поклонник Lombok, то это нормально. Просто добавьте конструктор, получатели и установщики самостоятельно.

Трансформация содержимого таблиц в Java-объекты

Так как POJO (простые объекты на Java) и компоненты Java проще в обращении внутри кода, чем общие типы коллекций из прошлой статьи (как насчет Map<String, Map<String, Integer>>), то хорошо бы иметь механизм, который превращает ряды таблиц в Gherkin-спецификациях в Java-объекты гибким, позволяющим повторное использование образом.

Встречайте тип таблиц данных, к которому можно получить доступ через аннотацию @DataTableType. Ниже – пример определения @DataTableType, регистрирующего трансформер, конвертирующий ряды таблиц из Gherkin-спецификации в экземпляры класса Book, заданного выше:

Since POJOs (Plain Old Java Objects) and Java beans are easier to deal with in code than the generic collection types we saw in the previous blog post (Map<String, Map<String, Integer>>, anyone?), it would be neat to have a mechanism available that could transform table rows in Gherkin specifications into Java objects, in a reusable and flexible way.

@DataTableType
public Book bookEntryTransformer(Map<String, String> row) {
 
    return new Book(
        row.get("title"),
        row.get("author"),
        Integer.parseInt(row.get("yearOfPublishing"))
    );
}

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

@Given("the following books")
public void theFollowingBooks(List<Book> books) {
 
    for(Book book: books) {
        System.out.printf(
            "'%s', published in %d, was written by %s\n",
            book.getTitle(),
            book.getYearOfPublishing(),
            book.getAuthor()
        );
    }
}

Как можно видеть, нам уже не нужно использовать сложный тип данных List<Map<String, String>>, с которым мы познакомились в прошлой статье, и итерации для обработки рядов таблицы становятся куда проще.

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

'To kill a mockingbird', published in 1960, was written by Harper Lee
'The catcher in the rye', published in 1951, was written by J.D. Salinger
'The great Gatsby', published in 1925, was written by F. Scott Fitzgerald

Заметка на полях: работа с пустыми ячейками

Иногда, создавая спецификации Gherkin, нужно явно заявить, что ячейка содержит пустое значение. Однако Cucumber-JVM по умолчанию интерпретирует пустые ячейки, как значения null, а не как пустые строки.

К счастью, это можно обойти: мы можем задать значение-плейсхолдер, которое превращается в пустую строку при создании @DataTableType – мы передаем его с использованием параметра replaceWithEmptyString:

@DataTableType(replaceWithEmptyString = "[anonymous]")
public Book bookEntryTransformer(Map<String, String> row) {
 
    // Этот метод не меняется, конвертация пустой строки происходит автоматически
}

Ряд таблицы Gherkin, заданный так:

Scenario: Listing book details
    Given the following books
        | title                                      | author                   | yearOfPublishing |
        | The life of Lazarillo de Tormes | [anonymous]         | 1544                  |

даст следующий результат:

'The life of Lazarillo de Tormes', published in 1544, was written by

Иными словами, именно то, что нам нужно (вместо автора – пустая строка).

Сравнение таблиц Gherkin с экземплярами объекта Java

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

Scenario: Listing book details
    Given the following books
        | title                                       | author                | yearOfPublishing |
        | To kill a mockingbird               | Harper Lee          | 1960                 |
        | The catcher in the rye            | J.D. Salinger         | 1951                 |
        | The great Gatsby                   | F. Scott Fitzgerald  | 1925                 |
        | The life of Lazarillo de Tormes | [anonymous]        | 1544                 |
    When I do nothing
    Then I expect to have the following books
        | title                                      | author                  | yearOfPublishing |
        | The life of Lazarillo de Tormes | [anonymous]         | 1544                 |
        | The great Gatsby                   | F. Scott Fitzgerald   | 1925                 |
        | To kill a mockingbird              | Harper Lee             | 1960                |
       | The catcher in the rye             | J.D. Salinger           | 1951                |

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

Вот возможное внедрение шага Then (думаю, из текста можно догадаться, что происходит на шаге When):

@Then("I expect to have the following books")
public void iExpectToHaveTheFollowingBooks(List<Book> expectedBooks) {
 
     Assert.assertTrue(CollectionUtils.isEqualCollection(expectedBooks, books));
}

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

Я решил воспользоваться внешней библиотекой commons-collections4 для сравнения. Есть и другие способы сравнивать списки Java-объектов, и ряд альтернатив перечислен здесь. Пожалуйста, учитывайте, что не все эти способы сработают для списков сложных объектов.

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

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

Как обычно, весь код из статьи можно найти на GitHub.

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