Contents

Cucumber 数据表简介

1. 简介

Cucumber 是一个行为驱动开发 (BDD) 框架,它允许开发人员使用 Gherkin 语言创建基于文本的测试场景。在许多情况下,这些场景需要模拟数据来执行功能,这可能很麻烦注入 - 特别是对于复杂或多个条目。

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

2. 场景语法

在定义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.数据表

虽然内联数据对于一本书就足够了,但当添加多本书时,我们的场景可能会变得混乱。为了处理这个问题,我们在场景中创建了一个数据表:

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

我们通过在 Given 子句文本下方缩进表格来将数据表定义为_Given_子句的一部分。使用此数据表,我们可以通过添加或删除行将任意数量的书籍(包括仅一本书)添加到我们的商店。

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

2.2. 包括标题

很明显,第一列代表书名,第二列代表书名。但是,每一列的含义并不总是那么明显。

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

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

虽然标题似乎只是表中的另一行,但当我们在下一节中将表解析为映射列表时,第一行具有特殊含义。

3. 步骤定义

创建场景后,我们实现_Given_步骤定义。对于包含数据表的步骤,我们使用**_DataTable_参数实现我们的方法**:

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

_DataTable_对象包含我们在场景中定义的数据表中的 表格数据,以及将这些数据转换为可用信息的方法。通常,在 Cucumber 中转换数据表有三种方法:(1)列表列表,(2)映射列表,以及(3)表转换器。

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

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

此外,我们将创建一个 管理_Book_对象的_BookStore_类 :

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

对于以下每个场景,我们将从基本步骤定义开始:

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.列表列表

处理表格数据的最基本方法是将_DataTable_参数转换为列表列表。我们可以创建一个没有表头的表格来演示:

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通过将每一行视为列值的列表,**将上表转换为列表列表。因此,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"]
]

我们使用_asLists_方法(提供_String.class_参数)将_DataTable_参数转换为 List<List>这个_Class_参数通知_asLists_方法我们期望每个元素是什么数据类型。在我们的例子中,我们希望标题和作者是 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)));
    }
}

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

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

3.2. Map列表

虽然List提供了从数据表中提取元素的基本机制,但步骤实现可能很神秘。Cucumber 提供了一个Map作为一种更易读的替代方案。

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

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

与列表机制类似,Cucumber 创建一个包含每一行的列表,但将列标题映射到每个列值。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"}
]

我们使用_asMaps_方法(提供两个_String.class_参数)将_DataTable_参数转换为 List<Map<String, String»第一个参数表示键(标题)的数据类型,第二个表示每列值的数据类型。因此,我们提供了两个_String.class_参数,因为我们的标题(键)和标题和作者(值)都是_String_。

然后我们遍历每个 _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. TableTransformer

将数据表转换为可用对象的最终(也是最丰富的)机制是创建一个_TableTransformer_。_TableTransformer_是一个对象,它指示 Cucumber 如何将_DataTable_对象转换为所需的域对象

/uploads/cucumber_data_tables/1.png

让我们看一个示例场景:

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

虽然带有键列数据的映射列表比列表列表更精确,但我们仍然使用转换逻辑来混淆我们的步骤定义。相反,我们应该使用所需的域对象(在本例中为_BookCatalog_)作为参数来定义我们的步骤:

@Given("^I have the following books in the store with transformer$")
public void haveBooksInTheStoreByTransformer(BookCatalog catalog) {
    store.addAllBooks(catalog.getBooks());
}

为此,我们必须创建 _TypeRegistryConfigurer_接口的自定义实现。

此实现必须执行两件事:

  1. 创建一个新的_TableTransformer_实现。
  2. 使用_configureTypeRegistry_方法注册这个新实现。

要将_DataTable_捕获到可用的域对象中,我们将创建一个 _BookCatalog_类:

public class BookCatalog {
 
    private List<Book> books = new ArrayList<>();

    public void addBook(Book book) {
        books.add(book);
    }
 
    // standard getter ...
}

要执行转换,让我们实现_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())
        );
    }
   //...

然后为我们的_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;
        }
    }
}

请注意,我们正在从表中转换英语数据,因此,我们从_locale()_方法返回英语语言环境。在不同的语言环境中解析数据时,我们必须将**_locale()_方法的返回类型更改为**适当的语言环境。

由于我们在场景中包含了数据表标题,因此在遍历表格单元格时必须跳过第一行(因此调用了 skip(1))。如果我们的表不包含标题,我们将删除_skip(1)_调用。

默认情况下,假设与测试关联的粘合代码 与运行器类位于同一包中。因此,如果我们将_BookStoreRegistryConfigurer_包含在与运行器类相同的包中,则不需要额外的配置。如果我们在不同的包中添加配置器,我们必须在运行器类的**_@CucumberOptions_字段中显式包含该包** 。