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_对象转换为所需的域对象:
让我们看一个示例场景:
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_接口的自定义实现。
此实现必须执行两件事:
- 创建一个新的_TableTransformer_实现。
- 使用_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_字段中显式包含该包** 。