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
в желаемый объект домена :
Давайте посмотрим на пример сценария:
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
.
Эта реализация должна выполнять две вещи:
- Создайте новую реализацию
TableTransformer
. - Зарегистрируйте эту новую реализацию с помощью метода
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 .