CakePHP
cakephp2
cakephp3
CakePHPDay 16

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

CakePHP Advent Calendar 2017の16日目です。
@keisukefunatsu さんの昨日の記事は、codeceptionをcakephp3で扱ってみるチュートリアルでした。

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

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

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

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

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

fixture を書くのが面倒

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

      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つは、公式ドキュメントでも紹介されている動的データとフィクスチャを利用することでも解決できます。
以下のように init() で記述できるので、 for 文で繰り返せば複数レコードの面倒からは解放されます。

    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 プラグインなどに追従するには、公式ドキュメントのテーブル情報のインポートを参照してください。

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

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

テストデータがテストケースから見えない(遠い)

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

例えば、公式ドキュメントのArticlesTable の published() 関数テストを参照してみましょう。

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 にある FabricationFactory Girl(注:現在は Factory Botに改名されています) のようなデータジェネレータがないのだ!、ということで作ったのが Fabricate です。

CakePHP2の方はFabricateを、CakePHP3の方はcakephp-fabricate-adaptorをご利用ください。

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

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

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

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

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

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 を使って書き換えると以下のように記述することができます。

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 を使って値をランダムに設定しています。気に入らない場合は、コールバック関数(この例では data )からreturnすることで変更できます。

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

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

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プラグインの紹介です。