«1. Введение

Cucumber — это среда разработки, управляемой поведением (BDD), которая позволяет разработчикам создавать текстовые тестовые сценарии с использованием языка Gherkin. Во многих случаях эти сценарии требуют фиктивных данных для реализации функции, внедрение которой может быть громоздким, особенно при наличии сложных или множественных записей.

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

2. Синтаксис сценария

При определении сценариев Cucumber мы часто вводим тестовые данные, используемые остальной частью сценария:

Scenario: Correct non-zero number of books found by author
  Given I have the a book in the store called The Devil in the White City by Erik Larson
  When I search for books by author Erik Larson
  Then I find 1 book

2.1. Таблицы данных

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

Scenario: Correct non-zero number of books found by author
  Given I have the following books in the store
    | The Devil in the White City          | Erik Larson |
    | The Lion, the Witch and the Wardrobe | C.S. Lewis  |
    | In the Garden of Beasts              | Erik Larson |
  When I search for books by author Erik Larson
  Then I find 2 books

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

Кроме того, таблицы данных можно использовать с любым предложением, а не только с предложением Given.

2.2. Включение заголовков

Очевидно, что первый столбец представляет название книги, а второй столбец представляет автора книги. Однако значение каждого столбца не всегда так очевидно.

Когда требуется разъяснение, мы можем включить заголовок, добавив новую первую строку:

Scenario: Correct non-zero number of books found by author
  Given I have the following books in the store
    | title                                | author      |
    | The Devil in the White City          | Erik Larson |
    | The Lion, the Witch and the Wardrobe | C.S. Lewis  |
    | In the Garden of Beasts              | Erik Larson |
  When I search for books by author Erik Larson
  Then I find 2 books

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

3. Определения шагов

После создания нашего сценария мы реализуем определение данного шага. В случае шага, содержащего таблицу данных, мы реализуем наши методы с аргументом DataTable:

@Given("some phrase")
public void somePhrase(DataTable table) {
    // ...
}

Объект DataTable содержит табличные данные из таблицы данных, которую мы определили в нашем сценарии, а также методы преобразования эти данные в полезную информацию. Как правило, в Cucumber существует три способа преобразования таблицы данных: (1) список списков, (2) список карт и (3) преобразователь таблицы.

Чтобы продемонстрировать каждый метод, мы будем использовать простой класс предметной области Book:

public class Book {

    private String title;
    private String author;

    // standard constructors, getters & setters ...
}

Кроме того, мы создадим класс BookStore, управляющий объектами Book:

public class BookStore {
 
    private List<Book> books = new ArrayList<>();
     
    public void addBook(Book book) {
        books.add(book);
    }
     
    public void addAllBooks(Collection<Book> books) {
        this.books.addAll(books);
    }
     
    public List<Book> booksByAuthor(String author) {
        return books.stream()
          .filter(book -> Objects.equals(author, book.getAuthor()))
          .collect(Collectors.toList());
    }
}

Для каждого из следующих сценариев , мы начнем с определения основного шага:

public class BookStoreRunSteps {

    private BookStore store;
    private List<Book> foundBooks;
    
    @Before
    public void setUp() {
        store = new BookStore();
        foundBooks = new ArrayList<>();
    }

    // When & Then definitions ...
}

3.1. Список списков

Самый простой метод обработки табличных данных — преобразование аргумента DataTable в список списков. Мы можем создать таблицу без заголовка для демонстрации:

Scenario: Correct non-zero number of books found by author by list
  Given I have the following books in the store by list
    | The Devil in the White City          | Erik Larson |
    | The Lion, the Witch and the Wardrobe | C.S. Lewis  |
    | In the Garden of Beasts              | Erik Larson |
  When I search for books by author Erik Larson
  Then I find 2 books

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

[
    ["The Devil in the White City", "Erik Larson"],
    ["The Lion, the Witch and the Wardrobe", "C.S. Lewis"],
    ["In the Garden of Beasts", "Erik Larson"]
]

Мы используем метод asLists — предоставляя аргумент String.class — для преобразования Аргумент DataTable для List\u003cList\u003cString\u003e\u003e. Этот аргумент Class сообщает методу asLists, какой тип данных мы ожидаем от каждого элемента. В нашем случае мы хотим, чтобы заголовок и автор были строковыми значениями. Таким образом, мы предоставляем String.class:

@Given("^I have the following books in the store by list$")
public void haveBooksInTheStoreByList(DataTable table) {
    
    List<List<String>> rows = table.asLists(String.class);
    
    for (List<String> columns : rows) {
        store.addBook(new Book(columns.get(0), columns.get(1)));
    }
}

Затем мы перебираем каждый элемент подсписка и создаем соответствующий объект Book. Наконец, мы добавляем каждый созданный объект Book в наш объект BookStore.

Если бы мы анализировали данные, содержащие заголовок, мы бы пропустили первую строку, поскольку Cucumber не различает заголовки и данные строки для списка списков.

3.2. Список карт

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

В этом случае мы должны указать заголовок для нашей таблицы:

Scenario: Correct non-zero number of books found by author by map
  Given I have the following books in the store by map
    | title                                | author      |
    | The Devil in the White City          | Erik Larson |
    | The Lion, the Witch and the Wardrobe | C.S. Lewis  |
    | In the Garden of Beasts              | Erik Larson |
  When I search for books by author Erik Larson
  Then I find 2 books

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

[
    {"title": "The Devil in the White City", "author": "Erik Larson"},
    {"title": "The Lion, the Witch and the Wardrobe", "author": "C.S. Lewis"},
    {"title": "In the Garden of Beasts", "author": "Erik Larson"}
]

«

«Мы используем метод asMaps, предоставляющий два аргумента String.class, для преобразования аргумента DataTable в List\u003cMap\u003cString, String\u003e\u003e. Первый аргумент обозначает тип данных ключа (заголовок), а второй указывает тип данных каждого значения столбца. Таким образом, мы предоставляем два аргумента String.class, потому что все наши заголовки (ключ), заголовок и автор (значения) являются строками.

@Given("^I have the following books in the store by map$")
public void haveBooksInTheStoreByMap(DataTable table) {
    
    List<Map<String, String>> rows = table.asMaps(String.class, String.class);
    
    for (Map<String, String> columns : rows) {
        store.addBook(new Book(columns.get("title"), columns.get("author")));
    }
}

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

3.3. Table Transformer

Последним (и наиболее богатым) механизмом преобразования таблиц данных в пригодные для использования объекты является создание TableTransformer. TableTransformer — это объект, который указывает Cucumber, как преобразовать объект DataTable в желаемый объект предметной области:

A transformer converts a DataTable into a desired object

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

Scenario: Correct non-zero number of books found by author with transformer
  Given I have the following books in the store with transformer
    | title                                | author      |
    | The Devil in the White City          | Erik Larson |
    | The Lion, the Witch and the Wardrobe | C.S. Lewis  |
    | In the Garden of Beasts              | Erik Larson |
  When I search for books by author Erik Larson
  Then I find 2 books

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

@Given("^I have the following books in the store with transformer$")
public void haveBooksInTheStoreByTransformer(BookCatalog catalog) {
    store.addAllBooks(catalog.getBooks());
}

Эта реализация должна выполнять две вещи:

Чтобы преобразовать DataTable в пригодный для использования объект домена, мы создадим класс BookCatalog:

  1. Create a new TableTransformer implementation.
  2. Register this new implementation using the configureTypeRegistry method.

Чтобы выполнить преобразование, давайте реализуем интерфейс TypeRegistryConfigurer: ~~ ~

public class BookCatalog {
 
    private List<Book> books = new ArrayList<>();
     
    public void addBook(Book book) {
        books.add(book);
    }
 
    // standard getter ...
}

и затем реализуем интерфейс TableTransformer для нашего класса BookCatalog:

public class BookStoreRegistryConfigurer implements TypeRegistryConfigurer {

    @Override
    public Locale locale() {
        return Locale.ENGLISH;
    }

    @Override
    public void configureTypeRegistry(TypeRegistry typeRegistry) {
        typeRegistry.defineDataTableType(
          new DataTableType(BookCatalog.class, new BookTableTransformer())
        );
    }

   //...

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

    private static class BookTableTransformer implements TableTransformer<BookCatalog> {

        @Override
        public BookCatalog transform(DataTable table) throws Throwable {

            BookCatalog catalog = new BookCatalog();
            
            table.cells()
              .stream()
              .skip(1)        // Skip header row
              .map(fields -> new Book(fields.get(0), fields.get(1)))
              .forEach(catalog::addBook);
            
            return catalog;
        }
    }
}

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

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

4. Заключение

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

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

Полный исходный код этой статьи можно найти на GitHub.

«