Tabelle dati cetriolo

1. Introduzione

Cucumber è un framework BDD (Behavioral Driven Development) che consente agli sviluppatori di creare scenari di test basati su testo utilizzando il linguaggio Gherkin. In molti casi, questi scenari richiedono dati fittizi per esercitare una funzionalità, che può essere complicata da iniettare, specialmente con voci complesse o multiple.

In questo tutorial, vedremo come utilizzare le tabelle di dati Cucumber per includere dati fittizi in modo leggibile.

2. Sintassi dello scenario

Quando definiamo gli scenari di Cucumber, spesso iniettiamo i dati di test utilizzati dal resto dello scenario:

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. Tabelle dati

Sebbene i dati in linea siano sufficienti per un singolo libro, il nostro scenario può diventare disordinato quando si aggiungono più libri. Per gestire questo, creiamo una tabella dati nel nostro scenario:

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

Definiamo la nostra tabella di dati come parte della nostra clausola Given facendo rientrare la tabella sotto il testo della clausola Given . Utilizzando questa tabella di dati, possiamo aggiungere un numero arbitrario di libri, incluso un solo libro, al nostro negozio aggiungendo o rimuovendo righe.

Inoltre, le tabelle di dati possono essere utilizzate con qualsiasi clausola , non solo con le clausole date .

2.2. Compresi i titoli

È evidente che la prima colonna rappresenta il titolo del libro e la seconda colonna rappresenta l'autore del libro. Tuttavia, il significato di ogni colonna non è sempre così ovvio.

Quando è necessario un chiarimento, possiamo includere un'intestazione aggiungendo una nuova prima riga :

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

Mentre l'intestazione sembra essere solo un'altra riga della tabella, questa prima riga ha un significato speciale quando analizziamo la nostra tabella in un elenco di mappe nella sezione successiva.

3. Definizioni dei passaggi

Dopo aver creato il nostro scenario, implementiamo la definizione del passaggio dato . Nel caso di un passaggio che contiene una tabella dati, implementiamo i nostri metodi con un argomento DataTable :

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

L' oggetto DataTable contiene i dati tabulari della tabella dati che abbiamo definito nel nostro scenario, nonché metodi per trasformare questi dati in informazioni utilizzabili . In generale, ci sono tre modi per trasformare una tabella dati in Cucumber: (1) un elenco di elenchi, (2) un elenco di mappe e (3) un trasformatore di tabella.

Per dimostrare ogni tecnica, useremo una semplice classe di dominio Book :

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

Inoltre, creeremo una classe BookStore che gestisce gli oggetti 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()); } }

Per ciascuno dei seguenti scenari, inizieremo con una definizione del passaggio di 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. Elenco degli elenchi

Il metodo più semplice per la gestione dei dati tabulari consiste nella conversione dell'argomento DataTable in un elenco di elenchi. Possiamo creare una tabella senza un'intestazione per dimostrare:

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 converte la tabella sopra in un elenco di elenchi trattando ogni riga come un elenco dei valori di colonna . Pertanto, Cucumber analizza ogni riga in un elenco contenente il titolo del libro come primo elemento e l'autore come secondo:

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

Usiamo il metodo asLists , fornendo un argomento String.class , per convertire l' argomento DataTable in un List . Questo argomento Class informa il metodo asLists quale tipo di dati ci aspettiamo che sia ogni elemento . Nel nostro caso, vogliamo che il titolo e l'autore siano valori String . Pertanto, forniamo 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))); } }
    

Quindi iteriamo su ogni elemento del sottoelenco e creiamo un oggetto Book corrispondente . Infine, aggiungiamo ogni oggetto Book creato al nostro oggetto BookStore .

Se analizzassimo i dati contenenti un'intestazione, salteremmo la prima riga poiché Cucumber non distingue tra intestazioni e dati di riga per un elenco di elenchi.

3.2. Elenco delle mappe

Sebbene un elenco di elenchi fornisca un meccanismo fondamentale per estrarre elementi da una tabella dati, l'implementazione del passaggio può essere criptica. Cucumber fornisce un elenco di meccanismi di mappe come alternativa più leggibile.

In questo caso, dobbiamo fornire un'intestazione per la nostra tabella :

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

Simile al meccanismo dell'elenco di elenchi, Cucumber crea un elenco contenente ciascuna riga ma associa invece l'intestazione della colonna a ciascun valore di colonna . Il cetriolo ripete questo processo per ogni riga successiva:

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

Mentre un elenco di elenchi e un elenco di mappe sono sufficienti per le tabelle di base, un trasformatore di tabelle fornisce un meccanismo molto più ricco in grado di gestire dati più complessi.

Il codice sorgente completo di questo articolo può essere trovato su GitHub.