LoginSignup
18

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-12-17

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18