Gurkendatentabellen

1. Einleitung

Cucumber ist ein BDD-Framework (Behavioral Driven Development), mit dem Entwickler textbasierte Testszenarien in der Sprache Gherkin erstellen können. In vielen Fällen erfordern diese Szenarien Scheindaten, um eine Funktion auszuüben, deren Injektion schwierig sein kann - insbesondere bei komplexen oder mehrfachen Einträgen.

In diesem Tutorial erfahren Sie, wie Sie mithilfe von Gurkendatentabellen Scheindaten lesbar einschließen.

2. Szenariosyntax

Bei der Definition von Gurkenszenarien werden häufig Testdaten eingefügt, die vom Rest des Szenarios verwendet werden:

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. Datentabellen

Während Inline-Daten für ein einzelnes Buch ausreichen, kann unser Szenario beim Hinzufügen mehrerer Bücher unübersichtlich werden. Um dies zu handhaben, erstellen wir in unserem Szenario eine Datentabelle:

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

Wir definieren unsere Datentabelle als Teil unserer Given- Klausel, indem wir die Tabelle unter dem Text der Given- Klausel einrücken . Mithilfe dieser Datentabelle können wir unserem Geschäft eine beliebige Anzahl von Büchern - einschließlich nur eines einzigen Buches - hinzufügen, indem wir Zeilen hinzufügen oder entfernen.

Darüber hinaus können Datentabellen mit jeder Klausel verwendet werden - nicht nur mit Given- Klauseln.

2.2. Einschließlich Überschriften

Es ist offensichtlich, dass die erste Spalte den Titel des Buches darstellt und die zweite Spalte den Autor des Buches darstellt. Die Bedeutung jeder Spalte ist jedoch nicht immer so offensichtlich.

Wenn eine Klarstellung erforderlich ist, können wir eine Überschrift einfügen, indem wir eine neue erste Zeile hinzufügen :

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

Während der Header nur eine weitere Zeile in der Tabelle zu sein scheint, hat diese erste Zeile eine besondere Bedeutung, wenn wir unsere Tabelle im nächsten Abschnitt in eine Liste von Karten analysieren.

3. Schrittdefinitionen

Nach dem Szenario erstellen, implementieren wir die Gegeben Schritt Definition. Bei einem Schritt, der eine Datentabelle enthält, implementieren wir unsere Methoden mit einem DataTable- Argument :

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

Das DataTable- Objekt enthält die tabellarischen Daten aus der Datentabelle, die wir in unserem Szenario definiert haben, sowie Methoden zum Umwandeln dieser Daten in verwendbare Informationen . Im Allgemeinen gibt es drei Möglichkeiten, eine Datentabelle in Cucumber zu transformieren: (1) eine Liste von Listen, (2) eine Liste von Karten und (3) einen Tabellentransformator.

Um jede Technik zu demonstrieren, verwenden wir eine einfache Book- Domain-Klasse:

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

Darüber hinaus werden wir eine erstellen Bookstore - Klasse , die verwalteten Buch - Objekte:

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

Für jedes der folgenden Szenarien beginnen wir mit einer grundlegenden Schrittdefinition:

public class BookStoreRunSteps { private BookStore store; private List foundBooks; @Before public void setUp() { store = new BookStore(); foundBooks = new ArrayList(); } // When & Then definitions ... }

3.1. Liste der Listen

Die grundlegendste Methode zum Behandeln von Tabellendaten ist das Konvertieren des DataTable- Arguments in eine Liste von Listen. Wir können eine Tabelle ohne Kopfzeile erstellen, um Folgendes zu demonstrieren:

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 konvertiert die obige Tabelle in eine Liste von Listen, indem jede Zeile als Liste der Spaltenwerte behandelt wird . Daher analysiert Cucumber jede Zeile in einer Liste, die den Buchtitel als erstes Element und den Autor als zweites enthält:

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

Wir verwenden die asLists- Methode, die ein String.class- Argument liefert , um das DataTable- Argument in eine Liste zu konvertieren. Dieses Class- Argument informiert die asLists- Methode über den erwarteten Datentyp für jedes Element . In unserem Fall möchten wir, dass Titel und Autor Zeichenfolgenwerte sind. Daher liefern wir 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))); } }
    

Wir iterieren dann jedes Element der Unterliste und erstellen ein entsprechendes Buch - Objekt. Schließlich fügen wir jedes erstellte Buch Objekt zu unserem Bookstore - Objekt.

Wenn wir Daten analysieren, die eine Überschrift enthalten, überspringen wir die erste Zeile, da Cucumber für eine Liste von Listen nicht zwischen Überschriften und Zeilendaten unterscheidet.

3.2. Liste der Karten

Während eine Liste von Listen einen grundlegenden Mechanismus zum Extrahieren von Elementen aus einer Datentabelle bietet, kann die Schrittimplementierung kryptisch sein. Cucumber bietet eine Liste von Kartenmechanismen als besser lesbare Alternative.

In diesem Fall müssen wir eine Überschrift für unsere Tabelle angeben :

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

Ähnlich wie beim Listenlistenmechanismus erstellt Cucumber eine Liste mit jeder Zeile, ordnet jedoch die Spaltenüberschrift jedem Spaltenwert zu . Gurke wiederholt diesen Vorgang für jede nachfolgende Zeile:

[ {"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.

Während eine Liste von Listen und eine Liste von Karten für Basistabellen ausreichen, bietet ein Tabellentransformator einen viel umfangreicheren Mechanismus, der komplexere Daten verarbeiten kann.

Den vollständigen Quellcode dieses Artikels finden Sie auf GitHub.