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 にある Fabrication や Factory 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プラグインの紹介です。