こんにちは。プロダクト開発部の @su-kun1899 です。
今回はママリの CakePHP アプリケーションに Fixture Factories を導入した事例を紹介します。
Fixture Factories とは何か
Fixture Factories は、モデルやデータベースに依存するテストコードにおいて、テーブルの作成やデータ初期化を行うためのプラグインです。
github.com
CakePHP には元々 Fixture という仕組みが提供されていますが、 Fixture Factories はより柔軟に扱うことができます。
https://book.cakephp.org/4/en/development/testing.html#test-fixtures
導入したきっかけ
アプリケーションの規模が大きくなり、機能が増えてくると、テーブル(モデル)ごとに一律データを管理する Fixuture は管理や運用の負荷が高まっていきます。
ママリでもテストケースとデータの依存関係が強くなり、テストデータを少し変更すると既存のテストが壊れて修正が必要になるなど、気軽に変更できないことが多くなってきていました。
Fabricate を導入するなど対応は行っていましたが、関連データの生成やシーケンシャルな値の発行等でママリでのユースケースにはマッチせず、十分な解決策には至っていませんでした。
GitHub - sizuhiko/Fabricate: PHP data generator for Testing inspired on Fabrication and factory-girl from the Ruby world.
そこで、Fixture Factories を導入することにしました。
Fixture Factories のいいところ
一部ですが、特に便利だなと思っているところを紹介します。
公式が推している
公式ドキュメントでも明確に Fixture 肥大化時の解決手段として言及されています。
https://book.cakephp.org/4/ja/development/testing.html#id20
github.com
API が直感的かつ柔軟
主観を多分に含みますが、かなり使いやすく読みやすいと思います。
<?php
$article = ArticleFactory::make()->getEntity();
<?php
$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 に従って、スムーズに関連データの生成が行なえます。
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();
$table->hasOne('Authors')->setForeignKey('author_id');
return $table;
}
}
おわりに
今回は Fixture Factories の導入と、その便利な機能の一部について紹介しました。
かなり便利さを実感しており、どんどん活用していこうと思います!
PR
コネヒトでは一緒にテストを改善していく仲間を募集しています!
hrmos.co