Перейти к основному содержимому

Таблицы данных о огурцах

· 9 мин. чтения

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<List<String>> . Этот аргумент 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<Map<String, String>> . Первый аргумент обозначает тип данных ключа (заголовок), а второй указывает тип данных каждого значения столбца . Таким образом, мы предоставляем два аргумента String.class , потому что все наши заголовки (ключ), title и author (значения) являются String s.

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

@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. Стол трансформер

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

./e69216ca656d135fc920dc0943cf13c1.png

Давайте посмотрим на пример сценария:

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

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

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

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

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

  1. Создайте новую реализацию TableTransformer .
  2. Зарегистрируйте эту новую реализацию с помощью метода configureTypeRegistry .

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

public class BookCatalog {

private List<Book> books = new ArrayList<>();

public void addBook(Book book) {
books.add(book);
}

// standard getter ...
}

Для выполнения преобразования реализуем интерфейс TypeRegistryConfigurer :

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())
);
}

//...

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

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;
}
}
}

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

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

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

4. Вывод

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

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

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