Tablas de datos de pepino

1. Introducción

Cucumber es un marco de desarrollo impulsado por el comportamiento (BDD) que permite a los desarrolladores crear escenarios de prueba basados ​​en texto utilizando el lenguaje Gherkin. En muchos casos, estos escenarios requieren datos simulados para ejercer una función, que puede ser engorroso de inyectar, especialmente con entradas complejas o múltiples.

En este tutorial, veremos cómo usar las tablas de datos de Cucumber para incluir datos simulados de manera legible.

2. Sintaxis del escenario

Al definir escenarios de pepino, a menudo inyectamos datos de prueba utilizados por el resto del escenario:

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. Tablas de datos

Si bien los datos en línea son suficientes para un solo libro, nuestro escenario puede volverse desordenado al agregar varios libros. Para manejar esto, creamos una tabla de datos en nuestro escenario:

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

Definimos nuestra tabla de datos como parte de nuestra cláusula dada al sangrar la tabla debajo del texto de la cláusula dada . Con esta tabla de datos, podemos agregar una cantidad arbitraria de libros, incluido un solo libro, a nuestra tienda agregando o eliminando filas.

Además, las tablas de datos se pueden utilizar con cualquier cláusula - no sólo Teniendo en cuenta las cláusulas.

2.2. Incluyendo títulos

Es evidente que la primera columna representa el título del libro y la segunda columna representa al autor del libro. Sin embargo, el significado de cada columna no siempre es tan obvio.

Cuando se necesita una aclaración, podemos incluir un encabezado agregando una nueva primera fila :

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

Si bien el encabezado parece ser solo otra fila de la tabla, esta primera fila tiene un significado especial cuando analizamos nuestra tabla en una lista de mapas en la siguiente sección.

3. Definiciones de pasos

Después de crear nuestro escenario, implementamos la definición de paso dado . En el caso de un paso que contiene una tabla de datos, implementamos nuestros métodos con un argumento DataTable :

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

El objeto DataTable contiene los datos tabulares de la tabla de datos que definimos en nuestro escenario, así como los métodos para transformar estos datos en información utilizable . Generalmente, hay tres formas de transformar una tabla de datos en Cucumber: (1) una lista de listas, (2) una lista de mapas y (3) un transformador de tabla.

Para demostrar cada técnica, usaremos una clase de dominio de libro simple :

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

Además, crearemos una clase BookStore que gestiona objetos 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()); } }

Para cada uno de los siguientes escenarios, comenzaremos con una definición de paso básica:

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

3.1. Lista de listas

El método más básico para manejar datos tabulares es convertir el argumento DataTable en una lista de listas. Podemos crear una tabla sin encabezado para demostrar:

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

Pepino convierte la tabla anterior en una lista de listas tratando cada fila como una lista de valores de columna . Por lo tanto, Cucumber analiza cada fila en una lista que contiene el título del libro como primer elemento y el autor como segundo:

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

Usamos el método asLists , que proporciona un argumento String.class , para convertir el argumento DataTable en una List . Este argumento de clase informa al método asLists qué tipo de datos esperamos que sea cada elemento . En nuestro caso, queremos que el título y el autor sean valores de cadena . Por lo tanto, proporcionamos 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))); } }
    

Luego iteramos sobre cada elemento de la sublista y creamos un objeto Book correspondiente . Por último, agregamos cada objeto Book creado a nuestro objeto BookStore .

Si analizamos los datos que contienen un encabezado, omitiríamos la primera fila ya que Pepino no diferencia entre encabezados y datos de fila para una lista de listas.

3.2. Lista de mapas

Si bien una lista de listas proporciona un mecanismo fundamental para extraer elementos de una tabla de datos, la implementación de pasos puede ser críptica. Cucumber proporciona una lista de mecanismos de mapas como una alternativa más legible.

En este caso, debemos proporcionar un encabezado para nuestra tabla :

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

Similar al mecanismo de lista de listas, Cucumber crea una lista que contiene cada fila, pero en su lugar asigna el encabezado de columna a cada valor de columna . Pepino repite este proceso para cada fila posterior:

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

Si bien una lista de listas y una lista de mapas son suficientes para las tablas básicas, un transformador de tabla proporciona un mecanismo mucho más rico capaz de manejar datos más complejos.

El código fuente completo de este artículo se puede encontrar en GitHub.