Object MotherパターンとTest Data Builderパターン

# はじめに

## 背景と目的
テストフィクスチャのセットアップは面倒で大変です。特に、複雑なオブジェクト構造のテストデータを準備するのは手間がかかる上に、冗長な記述によってテストコードのノイズが大きくなり可読性の低下を招きます。  
本稿では、テストフィクスチャのセットアップのための代表的なデザインパターンである`Object Motherパターン`と`Test Data Builderパターン`を説明し、また、改良のためのアイデアをご紹介します。

## サンプルコード
Java+JUnit5の[サンプルコード](https://github.com/yonetty/test-fixture-patterns)をGitHubで公開しています。

# テストフィクスチャとは
繰り返してテストを実行した際に同じ結果が得られることを保証するための一定の状態（環境やデータ）のことを`テストフィクスチャ`と呼びます。本投稿はその中でも、テスト実行に必要なデータのセットアップに焦点を当てます。

# サンプル
説明用のサンプルとして、図書管理アプリケーションを想定します。図書貸出管理サービス`LoanService`をテスト対象（SUT:System Under Test)とし、貸出時のチェックを行う`check`メソッドを検証する以下のテストコードがあったとします。

```java:SimpleLoanServiceTest.java
public class SimpleLoanServiceTest {

    private static final LocalDate TODAY = LocalDate.now();
    private static final LocalDate YESTERDAY = TODAY.minusDays(1);
    private static final LocalDate A_WEEK_AGO = TODAY.minusDays(7);

    private LoanService sut;

    @BeforeEach
    void setup() {
        sut = new LoanService(TODAY);
    }
...
    @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, TODAY);

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

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

        Book[] books = new Book[]{new Book("竹取物語"), new Book("平家物語")};

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

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

    @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節でテストデータをセットアップするコードが冗長かつテストケース間で重複している。
    + 実際のプロダクションコードではもっと多くのオブジェクト、プロパティのセットアップが必要。
+ テストケースとは無関係な値がノイズとなってテストの意図が汲み取りにくい。
    + 例えば、本のタイトル`枕草子`や日付定数`TODAY`がそのテストで意味のある値なのか単なるプレースホルダなのかわからない。

これらの問題点をどのように解決すればよいでしょうか。

# Object Motherパターン
単純にコードの重複を排除するということであれば、テストデータのセットアップ処理をテストクラス内にprivateメソッドとして切り出す手もありますが、複数のテストクラスに同じようなセットアップコードが重複して記述されてしまう危険性があります。  
テストデータをセットアップするファクトリメソッドを一箇所に集めたのが`Object Mother`です。  
以下は`User`をセットアップするファクトリメソッドを提供する`Object Mother`です。

```java:UserMother.java
public class UserMother {

    public static User aUserWithoutLoans() {
        Loans loans = Loans.empty();
        return new User("USER", LocalDate.MAX, loans);
    }

    public static User aUserBorrowing2Books() {
        Loans loans = Loans.of(aLoan(), aLoan());
        return new User("USER", LocalDate.MAX, loans);
    }

    public static User aUserBorrowing3Books() {
        Loans loans = Loans.of(aLoan(), aLoan(), aLoan());
        return new User("USER", LocalDate.MAX, loans);
    }
...
```

テストケースは以下のようになります。

```java:ObjectMotherLoanServiceTest.java
    @Test
    void 最大貸出数を超過する場合は貸出不可となる() {
        // Arrange
        User user = UserMother.aUserBorrowing2Books();

        Book[] books = BookMother.books(2);

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

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

`Object Mother`の特長として、ファクトリメソッドを一箇所にまとめることでテストクラスやテストケースをまたがっての再利用を促進する点が挙げられますが、最大の効用は`データのパターンに名前付けを行うこと`だと考えます。  
適切な名前付けを行うことで、テストコードの可読性を高めることができます。例えば、

```java
User user = UserMother.aUserBorrowing2Books();
```
というコードからはユーザー名や本のタイトル、日付といった（そのテストケースでは）ノイズとなる情報が排除され、「2冊の本を借りている任意のユーザー」というテスト条件が一目瞭然に読み取れるようになります。  
一方で、`Object Mother`には以下のデメリットもあります。

+ ファクトリメソッドが乱立し、`神クラス（God Class)`になりがち。
+ 複数のテストが`Object Mother`に依存するので、ファクトリメソッドを修正した場合に影響が広範に及ぶ。

# Test Data Builderパターン
`Test Data Builder`はGoFのデザインパターンのひとつである`Builderパターン`を使ってテストデータをセットする手法です。  
例を見ましょう。

```java:UserBuilder.java
public class UserBuilder {

    private String name = "DEFAULT_NAME";
    private LocalDate expirationDate = LocalDate.MAX;
    private Loans loans = Loans.empty();

    public UserBuilder withLoans(Loans loans) {
        this.loans = loans;
        return this;
    }

    public UserBuilder withExpirationDate(LocalDate expirationDate) {
        this.expirationDate = expirationDate;
        return this;
    }

    public User build() {
        return new User(name, expirationDate, loans);
    }
}
```

テストコードは以下のようになります。

```java:TestDataBuilderLoanServiceTest.java
    @Test
    void 貸出中のものに返却期限切れがある場合は新たな貸出は不可となる() {
        // Arrange
        Loan loan1 = new LoanBuilder().build();
        Loan loan2 = new LoanBuilder().withDueDate(YESTERDAY).build();

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

        User user = new UserBuilder().withLoans(loans).build();

        Book book = new BookBuilder().build();

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

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

    }
```

一般に`Test Data Builder`は各プロパティのデフォルト値を定義しておき、デフォルト値を使ったオブジェクト生成は以下のようにBuilderを生成して`build`メソッドを呼び出すだけです。

```java
new LoanBuilder().build();
```

デフォルト値から変更したいプロパティがあれば、Builderを生成した後にメソッドチェーンでつなげて記述して、最後に`build`メソッドを呼び出します。

```java
User user = new UserBuilder().withLoans(loans).build();
```

`Test Data Builder`はデフォルト値の利用や、メソッドチェーンを使った`流暢なAPI`によってテストコードの見通しをよくしますが、以下の欠点もあります。

+ 再利用性はあまり高くない。
+ （最初のテストコードに比べると改善されてはいるが）テストコードの冗長さは残り、テストの意図が伝わりづらい。

# Test Data Builderの改良
`Test Data Builder`をベースとしつつ、その欠点を改善するテクニックを紹介します。  
まず、new演算子でのオブジェクト生成はノイズなので、コンストラクタはprivateに変更し代わりにstaticなファクトリメソッドを用意します。

```java:UserBuilder.java
    private UserBuilder() {}

    public static UserBuilder ofDefault() {
        return new UserBuilder();
    }
```

デフォルト値以外で、よく使用されるセットアップのパターンもファクトリメソッドで提供します。

```java:UserBuilder.java
    public static UserBuilder borrowing(int numOfBooks) {
        UserBuilder builder = new UserBuilder();
        List<Loan> loanList = new ArrayList<>();
        for (int i = 0; i < numOfBooks; i++) {
            Loan loan = LoanBuilder.ofDefault().build();
            loanList.add(loan);
        }
        builder.loans = Loans.of(loanList.toArray(new Loan[0]));
        return builder;
    }
```

このファクトリメソッドを利用してテストコードを記述すると、コードがすっきりするとともに、ファクトリメソッドの`名前付けによって意図が明確になる`効果があります。いわばDSL（ドメイン固有言語）を設計するようなものです。

```java:HybridLoanServiceTest.java
    @Test
    void 最大貸出数を超過する場合は貸出不可となる() {
        // Arrange
        User user = UserBuilder.borrowing(2).build();

        Book[] books = BookBuilder.ofDefault().build(2);

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

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

また、ネストされたオブジェクトのセットアップを柔軟にできるようにするために、コールバックを受け取るテクニックもあります。  
以下のサンプルは、`User`オブジェクトにネストされている`Loan`オブジェクトを、`LoanBuilder`を使ってセットアップするコールバックを受け取る例です。

```java:UserBuilder.java
   public UserBuilder borrowing(Consumer<LoanBuilder> callback) {
        LoanBuilder loanBuilder = LoanBuilder.ofDefault();
        callback.accept(loanBuilder);
        loans.add(loanBuilder.build());
        return this;
    }
```

`UserBuilder`側で`LoanBuilder`オブジェクトを生成し、受け取ったコールバックに貸し出すことから、このような実装パターンは`Loanパターン`と呼ばれます。※`Loan`という言葉が符合したのは、偶然です(^_^;)  
利用するテストコードサンプルは以下です。コールバックはラムダ式で記述します。

```java:HybridLoanServiceTest.java
    @Test
    void 貸出中のものに返却期限切れがある場合は新たな貸出は不可となる() {
        // Arrange
        User user = UserBuilder.ofDefault()
                    .borrowing(loanBuilder -> loanBuilder.noop())
                    .borrowing(loanBuilder -> loanBuilder.withDueDate(YESTERDAY))
                    .build();

        Book book = BookBuilder.ofDefault().build();

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

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

# まとめ
* 複雑なオブジェクト構造のテストデータのセットアップは冗長になりがちで、ベタに書いてしまうとテストコードの可読性が下がってしまいます。
* `Object Mother`や`Test Data Builder`といったテストフィクスチャのセットアップに関わるパターンを活用し、テストコードをクリーンに保ちましょう。