テストデータを簡単に作る方法

CakePHP Advent Calendar 2017の16日目です。
@keisukefunatsu さんの昨日の記事は、[codeceptionをcakephp3で扱ってみるチュートリアル](https://qiita.com/keisukefunatsu/items/9b57a6e29a93c764068e)でした。

CakePHPで自動テストを書いていますか？
ちょうど1週間前に9日目の記事として、[CakePHP3 のアプリケーションを Behat でテストする（update編）](https://qiita.com/sizuhiko/items/3441f9c3532892d15307)を書きました。これはE2Eですが、もちろんPHPUnit(など)を使った単体テストもあるでしょう。

CakePHPではfixtureという仕組みを使ってテストデータを作成できるようになっています。

この記事は、テストをするには避けては通れないテストデータについて、以下のような悩みを持っている方を対象とした内容になります。

- fixture にテストデータを書くのが面倒
- DBにカラムを追加したときに fixture を書き換えるのが面倒
- テストデータがテストケースから見えない（遠い）

この記事では公式ドキュメントの引用に `CakePHP3` を使っています。CakePHP2をご利用のかたは適宜読み替えてください。

## fixture を書くのが面倒

fixture にテストデータを書く場合は以下のように `records` へ連想配列で記述します。

```php
      public $records = [
          [
              'title' => 'First Article',
              'body' => 'First Article Body',
              'published' => '1',
              'created' => '2007-03-18 10:39:23',
              'modified' => '2007-03-18 10:41:31'
          ],
          [
              'title' => 'Second Article',
              'body' => 'Second Article Body',
              'published' => '1',
              'created' => '2007-03-18 10:41:23',
              'modified' => '2007-03-18 10:43:31'
          ],
          [
              'title' => 'Third Article',
              'body' => 'Third Article Body',
              'published' => '1',
              'created' => '2007-03-18 10:43:23',
              'modified' => '2007-03-18 10:45:31'
          ]
      ];
```

どのように面倒と感じるかというと

- （複数レコードが必要になったとき）各レコードにカラム名が必要
- テストに関係ないカラムも必須項目だと書かないといけない
- テストケース毎に必要なデータは違うので、ファイルが大量にできてしまう

このうち、最初の2つは、公式ドキュメントでも紹介されている[動的データとフィクスチャ](https://book.cakephp.org/3.0/ja/development/testing.html#id15)を利用することでも解決できます。
以下のように `init()` で記述できるので、 `for` 文で繰り返せば複数レコードの面倒からは解放されます。

```php
    public function init()
    {
        $this->records = [
            [
                'title' => 'First Article',
                'body' => 'First Article Body',
                'published' => '1',
                'created' => date('Y-m-d H:i:s'),
                'modified' => date('Y-m-d H:i:s'),
            ],
        ];
        parent::init();
    }
```

また最後の「テストケース毎に必要なデータは違うので、ファイルが大量にできてしまう」を解決するのに、複数レコードを作ってそれぞれに意味をもたせるようなことがあると、以下のような問題が発生するので、テストケースごとに分けた方が良いです。

- 無駄なデータが大量にできてテストデータのロードによってテストが遅くなる
- どのデータが、どのケースで必要なのかわからなくなる
- （実コードやテストコードの）修正によって関係のないテストケースが容易に壊れる

## DBにカラムを追加したときに fixture を書き換えるのが面倒

テスト用のDB定義を `migration` プラグインなどに追従するには、公式ドキュメントの[テーブル情報のインポート](https://book.cakephp.org/3.0/ja/development/testing.html#id16)を参照してください。

```php
class ArticlesFixture extends TestFixture
{
    public $import = ['table' => 'articles'];
}
```

これでカラムの情報は常に最新の状況に追従します。
でもテストデータは追従してくれないので、 `records` の内容を書き換える必要があるかもしれません。テストに直接関係ないカラムだったとしても、それが `NOT NULL` 制約のかかったカラムであったのなら必須です。
テストケース毎にfixtureを分割していたなら、なおさら大変です。

## テストデータがテストケースから見えない（遠い）

上記の課題は大変だけど、なんとか頑張れると思うのですが、この課題は難しいです。
テストケースでどのようなデータが入るのかわからないので、テストケースの可読性が下がるような気がします。

例えば、公式ドキュメントの[ArticlesTable の published() 関数テスト](https://book.cakephp.org/3.0/ja/development/testing.html#id19)を参照してみましょう。

```php
class ArticlesTableTest extends TestCase
{
    public $fixtures = ['app.articles'];

    public function setUp()
    {
        parent::setUp();
        $this->Articles = TableRegistry::get('Articles');
    }

    public function testFindPublished()
    {
        $query = $this->Articles->find('published');
        $this->assertInstanceOf('Cake\ORM\Query', $query);
        $result = $query->hydrate(false)->toArray();
        $expected = [
            ['id' => 1, 'title' => 'First Article'],
            ['id' => 2, 'title' => 'Second Article'],
            ['id' => 3, 'title' => 'Third Article']
        ];

        $this->assertEquals($expected, $result);
    }
}
```

どのレコードが published で、どのレコードが published でないので、期待値が expected のようになるのか、一目ではわかりません。

## Fabricate を使ってみてください

実際私が `とあるプロジェクト` で上記のような問題に遭遇したときに、何で Ruby on Rails にある [Fabrication](https://www.fabricationgem.org/) や [Factory Girl（注：現在は Factory Botに改名されています）](https://github.com/thoughtbot/factory_bot) のようなデータジェネレータがないのだ！、ということで作ったのが `Fabricate` です。

CakePHP2の方は[Fabricate](https://github.com/sizuhiko/Fabricate/tree/cakephp2)を、CakePHP3の方は[cakephp-fabricate-adaptor](https://github.com/sizuhiko/cakephp-fabricate-adaptor)をご利用ください。

2つの違いは、Fabricateは元々CakePHP2用に作り始めたのですが、他のフレームワークでも使えるようにしたいな、と思い Fabricate はコアエンジンにして、フレームワーク用のアダプターを作ることにしたため別れたということです。
ただどちらのCakePHP用バージョンもメンテしています(PR歓迎です）。
ここではCakePHP2/3どちらのバージョンもすべて `Fabricate` という呼び名で呼びます。

詳しい利用方法は各リポジトリのREADMEを読んでもらうとして、ここでは、これまでの課題が解決できるかにフォーカスしてみたいと思います。
READMEが英語でイヤ！という場合は、以下の記事が少し役に立つかもしれません。

- [Fabricateのversion2を作成しました](http://blog.open.tokyo.jp/2015/03/14/fabricate-v2.html)

Fabricateで記述する場合も、fixtureは必要です。以下の機能は fixture で解決するためです。

- テスト用テーブルの作成
- テストケース毎のデータクリーニング

fixtureのrecords定義は不要なので、空で大丈夫です。
たとえば以下のような感じです。

```php
class ArticlesFixture extends TestFixture
{

    public $import = ['table' => 'articles'];

    /**
     * Records
     *
     * @var array
     */
    public $records = [];

    public function init()
    {
        parent::init();
        Fabricate::define(['PublishedArticle', 'class'=>'Articles'], ['published'=>'1']);
        Fabricate::define(['UnPublishedArticle', 'class'=>'Articles'], ['published'=>'0']);
    }
}
```

では、先ほどの published の例を Fabricate を使って書き換えると以下のように記述することができます。

```php
class ArticlesTableTest extends TestCase
{
    public $fixtures = ['app.articles'];

    public function setUp()
    {
        parent::setUp();
        $this->Articles = TableRegistry::get('Articles');
    }

    public function testFindPublished()
    {
        $numberWords =
 ['First','Second','Third','Fouth','Fifth','Sixth'];
        $data = function () use (&$numberWords) {
            return ['title' => array_shift($numberWords).' Article'];
        };
        Fabricate::create('PublishedArticle', 3, $data);
        Fabricate::create('UnPublishedArticle', 3, $data);

        $query = $this->Articles->find('published');
        $this->assertInstanceOf('Cake\ORM\Query', $query);
        $result = $query->hydrate(false)->toArray();
        $expected = [
            ['id' => 1, 'title' => 'First Article'],
            ['id' => 2, 'title' => 'Second Article'],
            ['id' => 3, 'title' => 'Third Article']
        ];

        $this->assertEquals($expected, $result);
    }
}
```

最初のテストケースに合わせてタイトルを加工するため、少し準備が長くなりましたが、fixtureを使った場合との比較ができると思います。
publishedが0と1のレコードが3件づつ生成されて、タイトルが `First Article` から `Sixth Article` までの6件生成されます。

また、記述していないカラムについては、データ型から自動的に判定して [Faker](https://github.com/fzaninotto/Faker) を使って値をランダムに設定しています。気に入らない場合は、コールバック関数（この例では `data` ）からreturnすることで変更できます。

Fabricateを使うと、データベースのカラムが変更されても、テストに注力したいカラムの情報と件数だけにフォーカスしてテストデータを作ることができるようになります。
テストケースが壊れたりすることもありません。

現在では[Faker](https://github.com/fzaninotto/Faker)自体がORMとの連携を拡充していて、CakePHP3のエンティティもFakerだけで生成することができるようになっています。
ただ関連先のデータも同時に作るといったことは、まだできないようです。
FabricateはCakePHPのプラグインとして作られたので、関連先のデータを同時に生成することもできます。以下は一例です。

```php
Fabricate::create('User', function($data, $world) {
    return [
        'user' => 'taro',
        'Post' => $world->association('Post', 3, function($data, $world) {
            return ['title'=>$world->sequence('Post.title',function($i){ return "Title-${i}"; })];
        }),
    ];
});
```

このように記述すると、Userを1件作って、そのユーザーに3件のPostを関連づけて作成することができます。

Fabricateを使うことで、すこしでもテストが書きやすくなる手助けになれば幸いです。

明日は @nojimage さんの[2017年に公開した自作CakePHPプラグインの紹介](https://php-tips.com/2017/12/17/my-cakephp-plugins-2017/)です。