もがき系プログラマの日常

もがき系エンジニアの勉強したこと、日常のこと、気になっている技術、備忘録などを紹介するブログです。

cakephpのtestをもっとやりやすく

はじめに

こんばんは。

簡単な記事ですが、今回やった対応をブログにします。

現在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データを少し修正するだけで、大量のテストがコケる可能性があります。

これをなんとか解決したいと思い、色々調べていると、似たようなことを考えている人がいました。

qiita.com

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とかは必要ないかなと思い、これを使うようにしました。

現場からは以上です。