Таблици с данни за краставици

1. Въведение

Краставицата е рамка за поведенческо развитие (BDD), която позволява на разработчиците да създават текстови тестови сценарии, използващи езика корнишън. В много случаи тези сценарии изискват фалшиви данни за упражняване на функция, която може да бъде тромава за инжектиране - особено при сложни или множество записи.

В този урок ще разгледаме как да използваме таблици с данни за краставици, за да включим фиктивни данни по четим начин.

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

Когато дефинираме сценарии за краставици, често инжектираме тестови данни, използвани от останалата част от сценария:

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

Ние определяме нашата таблица с данни, като част от нашата Предвид клауза от редовете на таблицата под текста на Имайки предвид клаузата . Използвайки тази таблица с данни, можем да добавим произволен брой книги - включително само една книга - към нашия магазин чрез добавяне или премахване на редове.

Освен това таблиците с данни могат да се използват с всяка клауза - не само с дадени клаузи.

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 съдържа табличните данни от таблицата с данни, която дефинирахме в нашия сценарий, както и методи за трансформиране на тези данни в използваема информация . Като цяло има три начина за трансформиране на таблица с данни в Краставица: (1) списък със списъци, (2) списък с карти и (3) трансформатор на таблици.

За да демонстрираме всяка техника, ще използваме прост клас на домейн на книга :

public class Book { private String title; private String author; // standard constructors, getters & setters ... }

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

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

За всеки от следните сценарии ще започнем с основна дефиниция на стъпка:

public class BookStoreRunSteps { private BookStore store; private List 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

Краставицата преобразува горната таблица в списък със списъци, като третира всеки ред като списък със стойностите на колоните . По този начин, Краставицата анализира всеки ред в списък, съдържащ заглавието на книгата като първи елемент и автора като втори:

[ ["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 в Списък . Този аргумент на клас информира метода asLists какъв тип данни очакваме да бъде всеки елемент . В нашия случай искаме заглавието и авторът да бъдат String стойности. По този начин ние доставяме String.class :

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

След това итерираме над всеки елемент от под-списъка и създаваме съответния обект Book . И накрая, ние добавяме всеки създаден обект Book към нашия обект BookStore .

Ако анализираме данни, съдържащи заглавие, ще пропуснем първия ред, тъй като Cucumber не прави разлика между заглавия и данни за редове за списък със списъци.

3.2. Списък на Карти

Докато списъкът със списъци предоставя основен механизъм за извличане на елементи от таблица с данни, изпълнението на стъпката може да бъде загадъчно. Краставицата предоставя списък с механизъм за карти като по-четима алтернатива.

В този случай трябва да предоставим заглавие за нашата таблица :

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 създава списък, съдържащ всеки ред, но вместо това засажда заглавието на колоната към всяка стойност на колоната . Краставицата повтаря този процес за всеки следващ ред:

[ {"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"} ]

We use the asMaps method — supplying two String.class arguments — to convert the DataTable argument to a List. The first argument denotes the data type of the key (header) and second indicates the data type of each column value. Thus, we supply two String.class arguments because our headers (key) and title and author (values) are all Strings.

Then we iterate over each Map object and extract each column value using the column header as the key:

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

3.3. Table Transformer

The final (and most rich) mechanism for converting data tables to usable objects is to create a TableTransformer. A TableTransformer is an object that instructs Cucumber how to convert a DataTable object to the desired domain object:

Let's see an example scenario:

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

While a list of maps, with its keyed column data, is more precise than a list of lists, we still clutter our step definition with conversion logic. Instead, we should define our step with the desired domain object (in this case, a BookCatalog) as an argument:

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

To do this, we must create a custom implementation of the TypeRegistryConfigurer interface.

This implementation must perform two things:

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

To capture the DataTable into a useable domain object, we'll create a BookCatalog class:

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

To perform the transformation, let's implement the TypeRegistryConfigurer interface:

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

and then implement the TableTransformer interface for our BookCatalog class:

 private static class BookTableTransformer implements TableTransformer { @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; } } }

Note that we're transforming English data from the table, and therefore, we return the English locale from our locale() method. When parsing data in a different locale, we must change the return type of the locale() method to the appropriate locale.

Since we included a data table header in our scenario, we must skip the first row when iterating over the table cells (hence the skip(1) call). We would remove the skip(1) call if our table did not include a header.

By default, the glue code associated with a test is assumed to be in the same package as the runner class. Therefore, no additional configuration is needed if we include our BookStoreRegistryConfigurer in the same package as our runner class. If we add the configurer in a different package, we must explicitly include the package in the @CucumberOptionsglue field for the runner class.

4. Conclusion

In this article, we looked at how to define a Gherkin scenario with tabular data using a data table. Additionally, we explored three ways of implementing a step definition that consumes a Cucumber data table.

Докато списък със списъци и списък с карти са достатъчни за основните таблици, трансформаторът на таблици осигурява много по-богат механизъм, способен да обработва по-сложни данни.

Пълният изходен код на тази статия може да бъде намерен в GitHub.