コネヒト開発者ブログ

コネヒト開発者ブログ

CakePHP Fixture Factories を導入しました

こんにちは。プロダクト開発部の @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
// 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

コネヒトでは一緒にテストを改善していく仲間を募集しています!

hrmos.co