Cucumber Data Tables – 黄瓜数据表

最后修改: 2019年 11月 24日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Cucumber is a Behavioral Driven Development (BDD) framework that allows developers to create text-based test scenarios using the Gherkin language.

Cucumber是一个行为驱动开发(BDD)框架,允许开发人员使用Gherkin语言创建基于文本的测试场景。

In many cases, these scenarios require mock data to exercise a feature, which can be cumbersome to inject — especially with complex or multiple entries.

在许多情况下,这些场景需要模拟数据来行使一个功能,这可能是繁琐的注入 – 特别是复杂或多个条目。

In this tutorial, we’ll look at how to use Cucumber data tables to include mock data in a readable manner.

在本教程中,我们将看看如何使用Cucumber数据表,以可读的方式包含模拟数据。

2. Scenario Syntax

2.语法方案

When defining Cucumber scenarios, we often inject test data used by the rest of the scenario:

在定义Cucumber 场景时,我们经常注入场景的其他部分所使用的测试数据。

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. Data Tables

2.1. 数据表

While inline data suffices for a single book, our scenario can become cluttered when adding multiple books.

虽然内联数据对一本书来说已经足够了,但当增加多本书时,我们的方案会变得很杂乱。

To handle this, we create a data table in our 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

We define our data table as a part of our Given clause by indenting the table underneath the text of the Given clause. Using this data table, we can add an arbitrary number of books — including only a single book — to our store by adding or removing rows.

我们将数据表定义为Given子句的一部分,方法是在Given子句的文本下方将表格缩进。使用这个数据表,我们可以通过添加或删除行来向我们的商店添加任意数量的书–包括只有一本书。

Additionally, data tables can be used with any clause — not just Given clauses.

此外,数据表可以与任何子句一起使用–不仅仅是Given子句。

2.2. Including Headings

2.2.包括标题

It’s clear that the first column represents the title of the book, and the second column represents the author of the book. The meaning of each column is not always so obvious, though.

很明显,第一栏代表书名,第二栏代表书的作者。不过,每一栏的含义并不总是那么明显。

When clarification is needed, we can include a header by adding a new first row:

当需要澄清时,我们可以通过添加一个新的第一行来包括一个标题

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

While the header appears to be just another row in the table, this first row has a special meaning when we parse our table into a list of maps in the next section.

虽然表头看起来只是表中的另一行,但当我们在下一节将表解析为地图列表时,这第一行有特殊的意义

3. Step Definitions

3.步骤定义

After creating our scenario, we implement the Given step definition.

在创建我们的场景后,我们实现Given步骤的定义。

In the case of a step that contains a data table, we implement our methods with a DataTable argument:

在一个包含数据表的步骤中,我们用一个DataTable参数来实现我们的方法

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

The DataTable object contains the tabular data from the data table we defined in our scenario as well as methods for transforming this data into usable information. Generally, there are three ways to transform a data table in Cucumber: (1) a list of lists, (2) a list of maps and (3) a table transformer.

DataTable对象包含了我们在场景中定义的数据表中的表格数据,以及将这些数据转换为可用信息的方法。一般来说,在Cucumber中有三种方法可以转换数据表:(1)列表,(2)地图列表和(3)表格转换器。

To demonstrate each technique, we’ll use a simple Book domain class:

为了演示每种技术,我们将使用一个简单的Book域类。

public class Book {

    private String title;
    private String author;

    // standard constructors, getters & setters ...
}

Additionally, we’ll create a BookStore class that manages Book objects:

此外,我们将创建一个BookStore类,管理Book对象。

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

For each of the following scenarios, we’ll start with a basic step definition:

对于以下每种情况,我们将从一个基本的步骤定义开始。

public class BookStoreRunSteps {

    private BookStore store;
    private List<Book> foundBooks;
    
    @Before
    public void setUp() {
        store = new BookStore();
        foundBooks = new ArrayList<>();
    }

    // When & Then definitions ...
}

3.1. List of Lists

3.1.名单列表

The most basic method for handling tabular data is converting the DataTable argument into a list of lists.

处理表格数据的最基本方法是将DataTable参数转换为列表的列表。

We can create a table without a header to demonstrate:

我们可以创建一个没有表头的表格来演示。

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 converts the above table into a list of lists by treating each row as a list of the column values.

Cucumber将上述表格转换为列表的列表,将每一行视为列值的列表。

So, Cucumber parses each row into a list containing the book title as the first element and the author as the second:

因此,Cucumber将每一行解析为一个列表,其中书名是第一个元素,作者是第二个元素。

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

We use the asLists method — supplying a String.class argument — to convert the DataTable argument to a List<List<String>>. This Class argument informs the asLists method what data type we expect each element to be.

我们使用asLists方法–提供一个String.class参数–将DataTable参数转换为List<List<String>这个参数告知asLists方法我们希望每个元素是什么数据类型。

In our case, we want the title and author to be String values, so we supply String.class:

在我们的例子中,我们希望标题和作者是String值,所以我们提供String.class

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

We then iterate over each element of the sub-list and create a corresponding Book object. Lastly, we add each created Book object to our BookStore object.

然后我们遍历子列表的每个元素,并创建一个相应的Book对象。最后,我们将每个创建的Book对象添加到我们的BookStore对象。

If we parsed data containing a heading, we would skip the first row since Cucumber does not differentiate between headings and row data for a list of lists.

如果我们解析包含标题的数据,我们会跳过第一行,因为Cucumber对列表的标题和行数据不作区分。

3.2. List of Maps

3.2.地图清单

While a list of lists provides a foundational mechanism for extracting elements from a data table, the step implementation can be cryptic. Cucumber provides a list of maps mechanism as a more readable alternative.

虽然列表提供了一个从数据表中提取元素的基础机制,但步骤的实现可能很隐晦。Cucumber提供了一个列表映射机制,作为一个更可读的替代方案。

In this case, we must provide a heading for our table:

在这种情况下,我们必须为我们的表格提供一个标题

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 to the list of lists mechanism, Cucumber creates a list containing each row but instead maps the column heading to each column value.

与列表的列表机制类似,Cucumber创建了一个包含每一行的列表,但是,将列标题映射到每一列值。

Cucumber repeats this process for each subsequent row:

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<Map<String, String>>. The first argument denotes the data type of the key (header) and second indicates the data type of each column value. So, we supply two String.class arguments because our headers (key) and title and author (values) are all Strings.

我们使用asMaps方法–提供两个String.class参数–将DataTable参数转换为List<Map<String, String>第一个参数表示键(头)的数据类型,第二个参数表示每列值的数据类型。所以,我们提供了两个String.class参数,因为我们的头(键)和标题及作者(值)都是Strings。

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

然后我们遍历每个Map对象,以列头为键提取每一列的值。

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

3.3. Table Transformer

3.3.表变压器

The final (and most rich) mechanism for converting data tables to usable objects is to create a TableTransformer.

将数据表转换为可用对象的最后(也是最丰富的)机制是创建一个TableTransformer

A TableTransformer is an object that instructs Cucumber how to convert a DataTable object to the desired domain object:

一个TableTransformer一个指示Cucumber如何将DataTable对象转换为所需领域对象的对象

 

A transformer converts a DataTable into a desired 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:

相反,我们应该以所需的领域对象(在本例中,是一个BookCatalog)作为参数来定义我们的步骤

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

要做到这一点,我们必须创建一个TypeRegistryConfigurer接口的自定义实现。

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:

为了将DataTable捕捉到一个可使用的领域对象,我们将创建一个BookCatalog类。

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

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

为了执行转换,我们来实现TypeRegistryConfigurer接口。

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 we’ll implement the TableTransformer interface for our BookCatalog class:

然后我们将为我们的BookCatalog类实现TableTransformer接口。

    private static class BookTableTransformer implements TableTransformer<BookCatalog> {

        @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 we therefore 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.

请注意,我们正在转换表中的英文数据,因此我们从locale()方法中返回英文语言。当解析不同语言的数据时,我们必须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.

由于我们在方案中包含了一个数据表的标题,我们在迭代表格单元格时必须跳过第一行(因此要调用skip(1))。如果我们的表不包括表头,我们将删除skip(1)调用。

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.

默认情况下,与测试相关的胶水代码被假定在与运行器类相同的包中。因此,如果我们将我们的BookStoreRegistryConfigurer纳入与运行器类相同的包中,就不需要额外的配置。

If we add the configurer in a different package, we must explicitly include the package in the @CucumberOptions glue field for the runner class.

如果我们在不同的包中添加配置器,我们必须在@CucumberOptions glue字段中明确包括该包,用于runner类

4. Conclusion

4.总结

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.

此外,我们探讨了实现消耗Cucumber数据表的步骤定义的三种方法。

While a list of lists and a list of maps suffice for basic tables, a table transformer provides a much richer mechanism capable of handling more complex data.

虽然一个列表和一个地图的列表足以满足基本的表,但表转换器提供了一个更丰富的机制,能够处理更复杂的数据。

The complete source code of this article can be found over on GitHub.

本文的完整源代码可以在GitHub上找到over