【CakePHP】Fixture Factoriesを使って増えすぎたFixtureをなんとかするお話

CakePHP

はじめに

こんにちは、株式会社レコチョクの早瀬です。
普段はmurketというECソリューションの開発をしています。
CakePHP 3.xのサポートが終わりCakePHP 4.xが主流になりましたが、CakePHP 4.3を機にテストコード周りに一部変更が入りました。
中でも公式が推奨しているFixture Factoriesに興味を持ったため、テストコードの変更点とともに調査してみました。

Fixtureの役割

CakePHP 4.3からDBのスキーマ管理とテストデータの管理の責務が分割されました。
以前はFixtureには “どういうカラムを持っているのか”を管理する $fieldと、”どういう値をテストデータとして登録しておくか”という $recordの2種類の情報を管理していました。
そのため、Fixtureはテーブル作成とデータ追加をテスト実行時に行っていましたが、最新版のCakePHPではテーブル作成はMigrationsに、データ追加はFixtureに役割が分割されました。

現状のFixtureの問題点

サービスが大規模になってくるとテストしなければいけない項目が多くなり、テストケースが増えていきます。
それに伴い、各テストケースで必要なテストデータを追加する必要があるため、Fixtureに登録されるテストデータは肥大化していきます。
CakePHPでは、テストケース実行前にテストデータをテスト用DBにINSERTし、テスト実行完了後にデータをTRUNCATEする仕組みになっています。
そのため、テストケースが増加する、もしくはテストデータが増加するとDBへのI/Oが増加し、テストの実行時間が長くなります。

CakePHPでは読み込むFixture情報をテストファイルのメンバ変数で管理しており、これらはテーブル単位で指定されます。

100件のテストケースが書かれているテストファイルがあり、うち1件だけは特定のテーブル( TableA)を確認しなければならない場合を想定します。
このような場合、 TableAは1箇所しか使用していないにも関わらず、INSERT/TRUNCATEがテストケース分(100回)発生してしまうという問題があります。

loadFixtures()を使ってこの問題を回避する方法もありますが、テストが頻繁に修正されるようなプロジェクトではどこまでのFixtureを一括で読み込み、どこからのFixtureをテストケース単位で読み込むのかという明確な基準を作るのも難しいと思われます。
特定の箇所でしか使用されていないFixtureを loadFixtures()で読み込むと、そのFixtureが頻繁に呼び出されるようになった際に $fixturesに移動させる手間が発生します。
また、いままでは頻繁に使用されていたが今はたまにしか使用されていないFixtureを見分けるのは困難であるため、 loadFixtures()に切り替えるのは大変です。
そこで登場するのがFixture Factoriesです。

Fixture Factories とは

Fixture Factoriesは従来のFixtureのように事前にテストデータを登録しておくのではなく、テストケース内で必要なデータを作成することで、DBのI/Oを減らす手法です。
従来手法の問題点はテストケースごとに大量のデータをINSERT/TRUNCATEすることですが、その本質的な問題は特定のケースでしか使用しないテストデータが大量に生まれてしまうというところです。
Fixture Factoriesを用いることで、各テストケースで必要なテストデータをテストケース内で作成し、Fixtureに書かれるテストデータの量を減らすことが出来ます。

今回のFixture Factoriesの概要を説明するにあたって、TODOアプリの登録・削除APIを作成しました。
CakePHPのバージョンは4.4.13、PHPのバージョンは8.1.4です。
必要に応じて以下コードを参考にしてください。

https://github.com/d-hayase/todo

DB構成は以下のようになっています。
image-20230523095249940.png

Fixture Factoriesを使用するためにはテーブル単位でFactoryをbakeします。

bakeされたFactoryクラスには getRootTableRegistryName()setDefaultTemplate()が存在しますが、各テストケースで使用したいデフォルト値が決まっているものについては setDefaultTemplate()に記載しておきます。

Fixture Factoriesを用いたEntity作成/保存

Factoryクラスを生成した後に、テストケースで以下のように記載することでEntityを生成できます。

また、Entity生成時に値の変更や個数の変更も可能です。

Entityの保存は以下のように記述できます。

関連付けられたEntityを作成する

生成したEntityに紐付いたEntityを追加で作成する場合には withを使用します。
withはテーブルのアソシエーションに基づいて関連したEntityを生成します。
例えばBoardEntityと紐付いたTodoEntityを作成したい場合、BoardFactoryに以下のように記述すると生成できます。

Entityの関連付けをメソッド化しておくことでおくことで、データ構造がわかりやすくなるというメリットもあります。
例えば、以下のように事前にTodoに対して優先度カテゴリを付与するメソッドを作っておき、withTodo()内で処理を出し分けることも可能です。

Fixture Factoriesを用いたテスト実装

以下のメソッドについてテストコードを記載します。

Fixture Factoriesを用いることでテストケース内で簡単にEntityの生成・保存ができるようになります。

Fixture FactoriesとFixtureの使い分け

Fixture Factoriesを使ってテストケース内でテストデータを作成できました。
しかし、複数のテストケースをまたいで使用するテストデータを毎回生成するのは非効率です。
そのため、マスタデータのようなデータ更新が少ないデータや複数箇所にまたがって使用されるテストデータはFixtureに記載、各テストケースで使い捨てになる場合はFixture Factoriesで生成するといった方法を取るとDBのI/Oを減らせて良いと思います。

最後に

Fixtureファイルを小さくできるFixture FactoriesはFabricateのようなテスト高速化ツールとも相性が良いです。
テストを作成する前にFactoryを作成しなければならないという手間はありますが、DB構成が複雑な場合は事前にFixtureを作るロジックを組んでおけばテストデータ作成の手間が軽減されるので有用かと思われます。
できれば1行で欲しいデータがDBに登録されるとありがたいので、Fixture作成をどうやってラッピングしていくかが今後の課題です。

参考

CakePHP