こんにちは。プロダクト開発部の @su-kun1899 です。
今回はママリの CakePHP アプリケーションに Fixture Factories を導入した事例を紹介します。
Fixture Factories とは何か
Fixture Factories は、モデルやデータベースに依存するテストコードにおいて、テーブルの作成やデータ初期化を行うためのプラグインです。
CakePHP には元々 Fixture という仕組みが提供されていますが、 Fixture Factories はより柔軟に扱うことができます。
https://book.cakephp.org/4/en/development/testing.html#test-fixtures
導入したきっかけ
アプリケーションの規模が大きくなり、機能が増えてくると、テーブル(モデル)ごとに一律データを管理する Fixuture は管理や運用の負荷が高まっていきます。
ママリでもテストケースとデータの依存関係が強くなり、テストデータを少し変更すると既存のテストが壊れて修正が必要になるなど、気軽に変更できないことが多くなってきていました。
Fabricate を導入するなど対応は行っていましたが、関連データの生成やシーケンシャルな値の発行等でママリでのユースケースにはマッチせず、十分な解決策には至っていませんでした。
そこで、Fixture Factories を導入することにしました。
Fixture Factories のいいところ
一部ですが、特に便利だなと思っているところを紹介します。
公式が推している
公式ドキュメントでも明確に Fixture 肥大化時の解決手段として言及されています。
https://book.cakephp.org/4/ja/development/testing.html#id20
API が直感的かつ柔軟
主観を多分に含みますが、かなり使いやすく読みやすいと思います。
<?php // Entity を生成するとき $article = ArticleFactory::make()->getEntity();
<?php // Entity を永続化するとき $article = ArticleFactory::make()->persist();
<?php // フィールドを書き換えるとき (未指定のフィールドはデフォルト値で生成される) $article = ArticleFactory::make(['title' => 'Foo'])->getEntity(); $article = ArticleFactory::make()->setField('title', 'Foo')->getEntity(); $article = ArticleFactory::make()->patchData(['title' => 'Foo'])->getEntity();
<?php // テストデータを複数件まとめて作るとき $articles = ArticleFactory::make(2)->getEntities(); // フィールド書き換えと同時に行うこともできます $articles = ArticleFactory::make(['title' => 'Foo'], 3)->getEntities(); $articles = ArticleFactory::make(3)->setField('title', 'Foo')->getEntities();
関連データの生成が柔軟
モデルの Association に従って、スムーズに関連データの生成が行なえます。
# Bake コマンドで -m オプションを指定すると、関連データ生成のメソッドも生やしてくれます bin/cake bake fixture_factory -m Articles
<?php // 関連データをまとめて作る $country = CountryFactory::make()->withCities()->persist();
<?php // 関連データの値や件数も柔軟に変更できる $country = CountryFactory::make()->withCities(3)->persist(); $country = CountryFactory::make()->withCities(['is_capital' => true])->persist(); $country = CountryFactory::make()->withCities(['is_capital' => true], 3)->persist();
Faker が使える
初期データは Factory の setDefaultTemplate メソッドで定義するのですが、 Faker が使えるのでテストデータ生成がスムーズです。
<?php class ArticleFactory extends BaseFactory { // ~~~~ 略 ~~~~~ protected function setDefaultTemplate(): void { $this->setDefaultData(function(Generator $faker) { return [ 'title' => $faker->text(30), 'body' => $faker->text(1000), ]; }) ->withAuthors(2); } // ~~~~ 略 ~~~~~ }
呼び出し元で callback を使って利用することも可能です。
<?php $article = ArticleFactory::make( fn(ArticleFactory $factory, Generator $faker) => [ 'title' => $faker->text, ] )->persist();
ちなみに Faker は CakePHP 側の defaultLocale を参照するので、 ja_JP
を指定しておくと日本語のテストデータ生成もできます。
その他 Tips
使用するケースは限られるかもしれませんが、参考までに。
データを組み合わせごと複製する
特定の組み合わせのデータを、そのまま任意の数複製することができます。
下記の例では、 is_admin が true / false で 5 件ずつ、合計 10 件のテストデータが生成されます。
<?php $users = UserFactory::make( [ ['is_admin' => true], ['is_admin' => false], ], 5 )->persist();
テストのときだけ Association を書き換える
あまりないと思いますが、 Factory で getTable をオーバーライドすることで、 テストのときだけ有効な Association を定義することができます。
何らかの事情でプロダクションコードでは Association を定義できないが、テストの場合は Association をあるものとしてテストデータ生成を効率化したいケースなどで利用できます。
<?php class ArticleFactory extends BaseFactory { // ~~~~ 略 ~~~~~ public function getTable(): Table { $table = parent::getTable(); // 一時的に Association を定義 $table->hasOne('Authors')->setForeignKey('author_id'); return $table; } // ~~~~ 略 ~~~~~ }
おわりに
今回は Fixture Factories の導入と、その便利な機能の一部について紹介しました。
かなり便利さを実感しており、どんどん活用していこうと思います!
PR
コネヒトでは一緒にテストを改善していく仲間を募集しています!