はじめに
こんばんは。
簡単な記事ですが、今回やった対応をブログにします。
現在cakephp3の案件をお手伝いしています。
今回はcakephpで使用する testとfixtureについてです。
cakephpのfixtureファイルは以下のような感じで、table構造と テストデータを一緒に記述できます。
<?php namespace App\Test\Fixture; use Cake\TestSuite\Fixture\TestFixture; class UsersFixture extends TestFixture { public $import = ['table' => 'users']; public $records = [ [ 'id' => 1, 'name' => 'kojirock', 'email' => 'kojikoji@example.com', ], [ 'id' => 2, 'name' => 'kojirock2', 'email' => 'kojikoji2@example.com', ], [ 'id' => 3, 'name' => 'kojirock3', 'email' => 'kojikoji3@example.com', ], ]; }
そして、testcaseでfixtureを使用したい場合以下のように記述します。
<?php namespace App\Test\TestCase\Repository; use App\Repository\UsersRepository; use Cake\TestSuite\TestCase; class UsersRepositoryTest extends TestCase { public $fixtures = [ 'app.users', ]; /** * @test */ public function findUsers() { $results = (new UsersRepository( $this->getTableLocator()->get('Users'), ))->findUsers(); $this->assertCount(3, $results); } }
これはこれで便利なのですが、問題があります。
あまりほかから参照されない tableに関しては良いのですが、いろんな場所から参照されている tableだと、この fixtureデータのテストデータがどんどん膨れ上がっていきます。
例えばこの UsersRepositoryで Userの友達情報も取るようなメソッドを作った場合、 UserFriendFixture
的なfixtureができ、そのテストデータレコードも膨れていきます。
また、 testケースの $fixtures
に設定した段階でテストデータが入ってしまうので、本来テストしたいデータ
のみ使いたいのですが、この形ではそれはかないません。
そして、testケース毎に truncate insertを繰り返すので、大量のfixtureを使用する & テストケースが多い場合、テストの実行時間がどんどん伸びていってしまいます。
更にやっかいなのが、大量なfixtureデータになりそのfixtureに依存するテストケースが増えてきた場合、fixtureデータを少し修正するだけで、大量のテストがコケる可能性があります。
これをなんとか解決したいと思い、色々調べていると、似たようなことを考えている人がいました。
autoFixtures = false
にすることで、テストデータを入れるタイミングをずらせるようです。
ただし、これだけだと、すでに大量のテストデータをfixtureに登録している場合、loadFixtureした段階でドンと不要なテストデータが登録されます。
これを解決するためには、 Fixtureデータは以下のようにテーブルを作成するまでにとどめることが大事だと思いました。
<?php namespace App\Test\Fixture; use Cake\TestSuite\Fixture\TestFixture; class UsersFixture extends TestFixture { public $import = ['table' => 'users']; }
そして、テストデータは以下のように、各テストケース毎に愚直に入れていきます。
<?php namespace App\Test\TestCase\Repository; use App\Repository\UsersRepository; use Cake\TestSuite\TestCase; class UsersRepositoryTest extends TestCase { public $fixtures = [ 'app.users', ]; public $autoFixtures = false; public function setUp() { $this->loadFixtures('Users'); parent::setUp(); } /** * @test */ public function findUsers() { $testDatum = [ 'name' => 'kojirock', 'email' => 'kojikoji@example.com', ]; $table = $this->getTableLocator()->get('Users') $table->save($table->newEntity($testDatum)); $results = (new UsersRepository( $this->getTableLocator()->get('Users'), ))->findUsers(); $this->assertCount(1, $results); } }
すこし骨が折れますが、これで各テスト毎に独立したデータが作れ、かつ、テストをしたいデータのみを考えるだけで良くなります。
これ以外の対策として、今回のテストケース用のfixtureを別で用意するという対応もあります。
ただ、今回はこれは使いませんでした。
fixtureはtable作成に留めるべき
という設計で統一したかったからが理由です。
テストケース毎にfixtureを用意する手間も大変そう...というのも理由の一つです。
終わりに
余談ですが save()
を使ってると ruresや単一トランザクションの処理が入り、テストデータ挿入で無駄なsqlが流れてしまいます。 第2引数を指定すればOKですが、それも毎回はめんどくさいです。
というわけで自分は以下みたいな traitを用意して対応してます。
<?php namespace App\Test\TestCase; use Cake\Datasource\EntityInterface; use Cake\ORM\Table; trait TestDataSaveTrait { /** * @param Table $table * @param array $testDatum * @return EntityInterface */ private function save(Table $table, array $testDatum): EntityInterface { $entityOption = ['validate' => false]; $saveOption = ['checkRules' => false, 'atomic' => false]; return $table->save($table->newEntity($testDatum, $entityOption), $saveOption); } /** * @param Table $table * @param array $testData * @return EntityInterface[] * @throws \Exception */ private function saveMany(Table $table, array $testData): array { $entityOption = ['validate' => false]; $saveOption = ['checkRules' => false]; return $table->saveMany($table->newEntities($testData, $entityOption), $saveOption); } }
あくまでテストデータを挿入するためだけに使うものなので、validationとかは必要ないかなと思い、これを使うようにしました。
現場からは以上です。