CakePHP3でFixtureをテストケースごとに分けたときに、エラーで落ちないようにする

# CakePHP3でFixtureをテストケースごとに分けたときに、エラーで落ちないようにする

CakePHP3でテストケースごとにFixtureを分けたときに、原因不明のエラーで落ちて時間を大幅ロスしたので、対応方法を備忘録として残しておきます。

## バージョン

- PHP 7.1
- CakePHP 3.5

## やりたかったこと ... Fixtureをテストケースごとに分ける

あるテーブルが複数のクラスから呼び出されており、それぞれのテストで使用するテストデータをFixtureで用意していましたが、管理が煩雑なのでテストケースごとにFixtureを分けて管理しようと試みました。

#### Fixture

```php:tests/Fixture/UserModification/UsersFixture.php
<?php

namespace App\Test\Fixture\UserModification;

use Cake\TestSuite\Fixture\TestFixture;

class UsersFixture extends TestFixture
{
    public $table = 'users';

    public $import = ['table' => 'users'];

    public $records = [
        [
            // 更新テストケース用のレコード
            'user_name' => 'hoge', 
           ...
        ],
    ];
}
```

```php:tests/Fixture/UserDelition/UsersFixture.php
<?php

namespace App\Test\Fixture\UserDelition;

use Cake\TestSuite\Fixture\TestFixture;

class UsersFixture extends TestFixture
{
    public $table = 'users';

    public $import = ['table' => 'users'];

    public $records = [
        [
            // 削除テストケース用のレコード
            'user_name' => 'hoge', 
           ...
        ],
    ];
}
```

#### Fixtureを呼び出すテストケース

```php:testCase/UserModification/UserModificationTest.php
<?php

namespace App\Test\TestCase\UserModification;

use Cake\TestSuite\TestCase;

class UserModificationTest extends TestCase
{
    public $fixtures = [
        'app.UserModification/UsersFixture', // ディレクトリ名を付ける
    ];
}
```

## エラー発生

テストケースはすべてエラーなしで通るものの、最後に謎のSQLエラーが発生するようになりました。

```
$ vendor/bin/phpunit src/TestCase

..... 5 / 5 (100%)Exception: SQLSTATE[25P02]: In failed sql transaction: 7 ERROR:  current transaction is aborted, commands ignored until end of transaction block in [/var/www/html/vendor/cakephp/cakephp/src/Database/Statement/StatementDecorator.php, line 174]
```

## 原因

CakePHP3の公式ページを参照すると（[こちら](https://book.cakephp.org/3.0/ja/development/testing.html#test-fixtures)）下記のように書かれています。

> CakePHP はフィクスチャーに基づいたテストケースを実行するにあたり、以下の動作をします。
> 
> 1. 各フィクスチャーで必要なテーブルを作成します。
> 2. フィクスチャーにデータが存在すれば、それをテーブルに投入します。
> 3. テストメソッドを実行します。
> 4. フィクスチャーのテーブルを空にします。
> 5. データベースからフィクスチャーのテーブルを削除します。

この 5. テーブル削除の時に、複数のFixtureで1つのテーブルを使用していると、複数回`DROP TABLE`するSQLが実行され、エラーになっていました。

## 解消方法

上記のテーブル削除は、CakePHPの中にある`FixtureManager`で実行されていたので、ここをどうにかすればよさそうでした。
本当は`DROP TABLE`する前にテーブルの存在チェックをすればいいかなと思ったのですが、

- CakePHP本体に手を入れる必要がある
- SQL標準でテーブルの存在チェックがないので、DBに依存してしまう

ので断念。

他のいい手があまりに思いつかなかったので`FixtureManager`の`$_load`プロパティ（使用したFixtureを管理している変数）から該当のFixtureを除外することで無理やり解決しました。。。

```php:UserModificationTest.php
...
public function tearDown()
{
    parent::tearDown();

    // ここでFixtureで登録されたレコードはtruncateしてしまう
    $this->fixtureManager->unload($this);

    $class = new \ReflectionClass(get_class($this->fixtureManager));
    $loaded = $class->getProperty('_loaded');
    $loaded->setAccessible(true);
    $loadedValue = $loaded->getValue($this->fixtureManager);
    foreach ($loadedValue as $fixtureName => $fixture) {
        if (in_array($fixtureName, $this->fixtures)) {
            unset($loadedValue[$fixtureName]);
        }
    }
    $loaded->setValue($this->fixtureManager, $loadedValue);
}
```

無理やり解決した感がすごいので、もっといい方法があるという方は是非教えてください。