Edited at
CakePHPDay 16

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

More than 1 year has passed since last update.

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