Tableaux de données sur le concombre

1. Introduction

Cucumber est un framework BDD (Behavioral Driven Development) qui permet aux développeurs de créer des scénarios de test basés sur du texte en utilisant le langage Gherkin. Dans de nombreux cas, ces scénarios nécessitent des données factices pour exercer une fonctionnalité, ce qui peut être fastidieux à injecter - en particulier avec des entrées complexes ou multiples.

Dans ce didacticiel, nous verrons comment utiliser les tables de données Cucumber pour inclure des données fictives de manière lisible.

2. Syntaxe du scénario

Lors de la définition des scénarios Cucumber, nous injectons souvent des données de test utilisées par le reste du scénario:

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. Tableaux de données

Alors que les données en ligne suffisent pour un seul livre, notre scénario peut devenir encombré lors de l'ajout de plusieurs livres. Pour gérer cela, nous créons une table de données dans notre scénario:

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

Nous définissons notre table de données comme une partie de notre clause Given en indentant la table sous le texte de la clause Given . En utilisant cette table de données, nous pouvons ajouter un nombre arbitraire de livres - y compris un seul livre - à notre boutique en ajoutant ou en supprimant des lignes.

De plus, les tables de données peuvent être utilisées avec n'importe quelle clause - pas seulement les clauses Given .

2.2. Y compris les titres

Il est évident que la première colonne représente le titre du livre et la deuxième colonne représente l'auteur du livre. Cependant, la signification de chaque colonne n'est pas toujours aussi évidente.

Lorsqu'une clarification est nécessaire, nous pouvons inclure un en-tête en ajoutant une nouvelle première ligne :

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

Alors que l'en-tête semble n'être qu'une autre ligne du tableau, cette première ligne a une signification particulière lorsque nous analysons notre tableau en une liste de cartes dans la section suivante.

3. Définitions des étapes

Après avoir créé notre scénario, nous implémentons la définition d'étape donnée . Dans le cas d'une étape contenant une table de données, nous implémentons nos méthodes avec un argument DataTable :

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

L' objet DataTable contient les données tabulaires de la table de données que nous avons définie dans notre scénario, ainsi que des méthodes pour transformer ces données en informations utilisables . En règle générale, il existe trois façons de transformer une table de données dans Cucumber: (1) une liste de listes, (2) une liste de cartes et (3) un transformateur de table.

Pour illustrer chaque technique, nous utiliserons une classe de domaine Book simple :

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

De plus, nous allons créer une classe BookStore qui gère les objets 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()); } }

Pour chacun des scénarios suivants, nous commencerons par une définition d'étape de base:

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 des listes

La méthode la plus basique pour gérer les données tabulaires consiste à convertir l' argument DataTable en une liste de listes. Nous pouvons créer un tableau sans en-tête pour démontrer:

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 convertit le tableau ci-dessus en une liste de listes en traitant chaque ligne comme une liste des valeurs de colonne . Ainsi, Cucumber analyse chaque ligne en une liste contenant le titre du livre comme premier élément et l'auteur comme second:

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

Nous utilisons la méthode asLists - fournissant un argument String.class - pour convertir l' argument DataTable en List . Cet argument de classe informe la méthode asLists du type de données que nous attendons de chaque élément . Dans notre cas, nous voulons que le titre et l'auteur soient des valeurs String . Ainsi, nous fournissons 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))); } }
    

Nous parcourons ensuite chaque élément de la sous-liste et créons un objet Book correspondant . Enfin, nous ajoutons chaque objet Book créé à notre objet BookStore .

Si nous analysions les données contenant un en-tête, nous sauterions la première ligne car Cucumber ne fait pas la différence entre les en-têtes et les données de ligne pour une liste de listes.

3.2. Liste des cartes

Alors qu'une liste de listes fournit un mécanisme fondamental pour extraire des éléments d'une table de données, la mise en œuvre de l'étape peut être cryptique. Cucumber fournit une liste de mécanismes de cartes comme alternative plus lisible.

Dans ce cas, nous devons fournir un en-tête pour notre tableau :

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

Semblable au mécanisme de liste des listes, Cucumber crée une liste contenant chaque ligne mais mappe à la place l'en-tête de colonne à chaque valeur de colonne . Le concombre répète ce processus pour chaque rangée suivante:

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

Alors qu'une liste de listes et une liste de cartes suffisent pour les tables de base, un transformateur de table fournit un mécanisme beaucoup plus riche capable de gérer des données plus complexes.

Le code source complet de cet article se trouve à l'adresse over sur GitHub.