1. 序章

Cucumberは、開発者がGherkin言語を使用してテキストベースのテストシナリオを作成できるようにするBehavioral Driven Development(BDD)フレームワークです。 多くの場合、これらのシナリオでは、機能を実行するためにモックデータが必要です。これは、特に複雑なエントリや複数のエントリの場合、挿入するのが面倒な場合があります。

このチュートリアルでは、Cucumberデータテーブルを使用して、読みやすい方法でモックデータを含める方法を見ていきます。

2. シナリオ構文

キュウリシナリオを定義するとき、シナリオの残りの部分で使用されるテストデータを注入することがよくあります。

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. データテーブル

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のテキストの下にテーブルをインデントします。 このデータテーブルを使用すると、行を追加または削除することで、任意の数の本(1冊の本のみを含む)をストアに追加できます。

さらに、データテーブルは、 Given 句だけでなく、任意の句で使用できます。

2.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)テーブルトランスフォーマーの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は各行を解析して、最初の要素として本のタイトルを、2番目の要素として著者を含むリストを作成します。

[
    ["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 引数—変換するデータ表に対する議論リスト >>このクラス引数は、各要素がであると予想されるデータ型を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. マップのリスト

リストのリストは、データテーブルから要素を抽出するための基本的なメカニズムを提供しますが、ステップの実装は不可解な場合があります。 Cucumberは、より読みやすい代替手段としてマップメカニズムのリストを提供します。

この場合、テーブルの見出しを指定する必要があります

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 方法—2つを供給する String.class 引数—変換するデータ表に対する議論リスト

>>

最初の引数はキー(ヘッダー)のデータ型を示し、2番目の引数は各列の値のデータ型を示します。 したがって、ヘッダー(キー)とタイトルおよび作成者(値)はすべて String であるため、2つのString.class引数を指定します。

次に、各 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 は、DataTableオブジェクトを目的のドメインオブジェクトに変換する方法をCucumberに指示するオブジェクトです。

 

 

シナリオの例を見てみましょう。

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インターフェイスのカスタム実装を作成する必要があります

この実装では、次の2つのことを実行する必要があります。

  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()メソッドの戻り値のタイプを適切なlocale変更する必要があります。

シナリオにデータテーブルヘッダーを含めたため、テーブルセルを反復処理するときに最初の行をスキップする必要があります(したがって、 skip(1)呼び出し)。 テーブルにヘッダーが含まれていない場合は、 skip(1)呼び出しを削除します。

デフォルトでは、テストに関連付けられたグルーコードは、ランナークラスと同じパッケージに含まれていると見なされます。 したがって、ランナークラスと同じパッケージに BookStoreRegistryConfigurer を含める場合は、追加の構成は必要ありません。 別のパッケージにコンフィギュレーターを追加する場合は、ランナークラスの@CucumberOptions接着フィールドにパッケージを明示的に含める必要があります。

4. 結論

この記事では、データテーブルを使用して表形式のデータでGherkinシナリオを定義する方法について説明しました。 さらに、Cucumberデータテーブルを使用するステップ定義を実装する3つの方法を検討しました。

基本的なテーブルにはリストのリストとマップのリストで十分ですが、テーブルトランスフォーマーは、より複雑なデータを処理できるはるかに豊富なメカニズムを提供します。

この記事の完全なソースコードは、GitHubにあります。