Groovy流のTest Data Builderを使ってSpockのテストコードをもっと読みやすく

# はじめに
Javaアプリケーションのテスティングフレームワークには`Spock`をお勧めします。Groovy+Spockによってテストコードのノイズが減り可読性が向上し、テストの意図もわかりやすくなります。  
テストコードの中でも、テストデータのセットアップは冗長になりがちで、その解決策として`Object Mother`や`Test Data Builder`といったデザインパターンがあります。（これらのパターンの解説は[こちら](https://qiita.com/yonetty/items/1a11fff1520f292c4e86)  
本稿では、Groovyに標準で含まれる`FactoryBuilderSupport`というサポート機能を用いて、Groovy流の`Test Data Builder`を作成する方法をまとめます。  
なお、サンプルコードは[GitHub](https://github.com/yonetty/test-fixture-patterns)で公開しています。

# サンプル題材
図書管理システム(Java)の貸出サービスをテストする、JUnit 5で書かれた以下のようなテストコードがあったとします。

```java
    @Test
    void 貸出中のものに返却期限切れがある場合は新たな貸出は不可となる() {
        // Arrange
        Book book1 = new Book("枕草子");
        Loan loan1 = new Loan(book1, A_WEEK_AGO, TODAY);
        Book book2 = new Book("源氏物語");
        Loan loan2 = new Loan(book2, A_WEEK_AGO, YESTERDAY);

        Loans loans = Loans.of(loan1, loan2);

        User user = new User("山本", TODAY, loans);

        Book book = new Book("竹取物語");

        // Act
        LoanCheckResult result = sut.check(user, book);

        // Assert
        assertThat(result.hasError, is(true));
        assertThat(result.errorCode, is("UNRETURNED_BOOKS"));
    }
```

Arrange節でテストデータのセットアップを行っていて、以下のようなネスト構造の`User`オブジェクトを生成しています。

* `User`オブジェクトは`Loans`オブジェクトを持つ
* `Loans`オブジェクト（ファーストクラスコレクション）は、複数の`Loan`オブジェクトを持つ
* `Loan`オブジェクトは`Book`オブジェクトを持つ

サンプルなのでプロパティも少なく、これくらいなら比較的読めるのですが、実際のプロダクトでオブジェクトが複雑になると、行数も増えて非常に読みづらくなることがあります。

# Spockで書いたテストコード
この後説明するBuilderを用いて、以下のようにテストデータをセットアップできます。

```groovy
    def '貸出中のものに返却期限切れがある場合は新たな貸出は不可となる'() {
        given: "返却期限切れの貸出があるユーザー"
        def builder = new GroovyUserBuilder()
        def user = builder.user(name: '山本') {
            borrowing(loanDate: A_WEEK_AGO, dueDate: TODAY) {
                book(title: "枕草子")
            }
            borrowing(loanDate: A_WEEK_AGO, dueDate: YESTERDAY) {
                book(title: "源氏物語")
            }
        }

        and: "新規に借りたい本"
        def book = new Book('竹取物語')

        when: "チェックを実行"
        def result = sut.check(user, book)

        then: "チェックエラー"
        result.hasError
        result.errorCode == 'UNRETURNED_BOOKS'
    }
```

ポイントは以下です。

* Mapを使ってプロパティをセットすることで、キー名で何のプロパティかがわかる。

Javaのコンストラクタだと、引数が多い場合にどれが何を指しているのかわかりづらいですが、これなら一目瞭然です。

```groovy
builder.user(name: '山本')
```

* 途中のオブジェクトを省略できる。


ファーストクラスコレクションは素晴らしいOODの実装プラクティスですが、テストデータのセットアップ時には一手間余計に掛かってしまうので、ここではBuilder側に隠蔽しています。

```groovy
        def user = builder.user(name: '山本') {
            borrowing(loanDate: A_WEEK_AGO, dueDate: TODAY) {
```

* テストコードの表現力が増す。

`Loan`オブジェクトを`loan`と書く代わりに`borrowing`と動詞で表現することで、よりわかりやすく、DSL風になります。

```groovy
borrowing(loanDate: A_WEEK_AGO, dueDate: TODAY) {
```

# Test Data Builderの作成方法
実際にBuilderを作成する方法を説明します。

## Builder
`groovy.util.FactoryBuilderSupport`を継承したクラスを作成します。

```groovy
class GroovyUserBuilder extends FactoryBuilderSupport {

    GroovyUserBuilder() {
        registerFactory('user', new UserFactory())
        registerFactory('borrowing', new LoanFactory())
        registerFactory('book', new BookFactory())
    }
}
```

テストコードから利用するBuilder表現中の、各メソッド名に対応するFactory（後述）のインスタンスを、`registerFactory`メソッドで登録します。

対応を確認するため、テストコードのBuilder利用箇所を再掲しておきます。

```groovy
        def user = builder.user(name: '山本') {
            borrowing(loanDate: A_WEEK_AGO, dueDate: TODAY) {
                book(title: "枕草子")
            }
            borrowing(loanDate: A_WEEK_AGO, dueDate: YESTERDAY) {
                book(title: "源氏物語")
            }
        }
```

## Factory
名前からご察しのとおり、`FactoryBuilderSupport`は`Factoryパターン`を用いてオブジェクトの生成を行います。  
オブジェクトのツリー構造の個々のノードのオブジェクトを生成する責務を表すインタフェースが`groovy.util.Factory`です。通常は、そのデフォルト実装を提供する抽象基底クラスである`groovy.util.AbstractFactory`を継承したFactoryクラスを実装します。  

### LoanFactory
まずは簡単な`LoanFactory`で基本的な説明をします。

```groovy
class LoanFactory extends AbstractFactory {

    def newInstance(FactoryBuilderSupport builder, Object name, Object value, Map attributes) {
        attributes as Loan
    }

    void setChild(FactoryBuilderSupport builder, Object parent, Object child) {
        parent.book = child
    }
}
```

`newInstance`メソッドは、`Loan`クラスのインスタンスを生成するように実装します。このとき、テストコード側で

```groovy
borrowing(loanDate: A_WEEK_AGO, dueDate: TODAY)
```

の括弧内に記述したMapをメソッド引数`attributes`で受け取るので、それを使ってインスタンスを生成しています。

```groovy
attributes as Loan
```

上記のように、`Mapオブジェクト as クラス名`という記法によってMapの各々の値をプロパティとするインスタンスを生成できます。
※対象がJavaオブジェクトの場合、privateでもいいのでデフォルトコンストラクタを持っていることが条件

`setChild`メソッドをオーバーライドして、親子関係の関連付けを行うことができます。ここでは、`Loan`オブジェクトの子として`Book`オブジェクトをセットします。

```groovy
    void setChild(FactoryBuilderSupport builder, Object parent, Object child) {
        parent.book = child
    }

```

### BookFactory
`Book`は末端のノードに存在するオブジェクトなので、親子関係の設定は不要です。リーフノードであることを示す`isLeaf`メソッドが`true`を返すようにオーバーライドします。

```groovy
class BookFactory extends AbstractFactory {

    boolean isLeaf() {
        true
    }

    def newInstance(FactoryBuilderSupport builder, Object name, Object value, Map attributes) {
        attributes as Book
    }
}
```

### UserFactory
`UserFactory`は、`User`オブジェクトの子として存在する`Loans`を隠蔽するような実装となっています。

```groovy
class UserFactory extends AbstractFactory {

    static final DEFAULTS = [name: 'DUMMY', expirationDate: LocalDate.MAX, loans: Loans.empty()]

    def newInstance(FactoryBuilderSupport builder, Object name, Object value, Map attributes) {
        def props = DEFAULTS + attributes
        props as User
    }

    void setChild(FactoryBuilderSupport builder, Object parent, Object child) {
        parent.loans.add(child)
    }
}
```

具体的には、`setChild`メソッドで、子ではなく孫として追加するように実装しています。

```groovy
    void setChild(FactoryBuilderSupport builder, Object parent, Object child) {
        parent.loans.add(child)
    }
```

また、テストデータのデフォルト値を宣言しておくことで、テストケースでは特に問わないプロパティについては省略して記述できるようにしています。

```groovy
    static final DEFAULTS = [name: 'DUMMY', expirationDate: LocalDate.MAX, loans: Loans.empty()]
```

デフォルト値のMapと、渡されたMapをマージしてインスタンスを生成します。

```groovy
    def newInstance(FactoryBuilderSupport builder, Object name, Object value, Map attributes) {
        def props = DEFAULTS + attributes
        props as User
    }
```

`AbstractFactory`には他にもオーバーライド可能なメソッドが複数提供されています。興味がある方は調べてみてください。

# まとめ
* 本稿では、`FactoryBuilderSupport`による`Test Data Builder`の実装方法をサンプルコードを添えてご紹介しました。
* Groovyの機能を活用することで、Spockのテストコードがさらに表現力豊かになります。