Laravel 5 で Paratest を使ったら自動テストの実行時間が半分くらいになった

# 概要
Paratest という PHPUnit を並列(複数プロセスで同時)実行してくれるツールで自動テストを並列実行してみたところ、自動テストの実行速度が最大で半分くらいになりました。
毎回10分以上かかっていたのが5分台まで短くなりテスト気軽に流せるようになりました。
ただ、私のプロジェクトの環境では導入時に少し工夫が必要だったためその点について記載します。

https://github.com/paratestphp/paratest

### その前に：Laravel 8 以降を使っている方へ
テストを並列実行できる機能がデフォルトであるので下記をどうぞ！

https://readouble.com/laravel/8.x/ja/testing.html#running-tests-in-parallel

## 環境
- PHP 7.3
- Laravel 5.8
- PHPUnit 7.5
- Paratest 3.1.2
- PostgreSQL 11.6

※ Paratest のバージョンが古いのはプロジェクトで使っている PHPUnit のバージョンに合わせたためです。

# 課題
Paratest を導入しても何も設定を変えずに実行はできませんでした。
大きく2つ課題があったのでその対処法について説明します。

- DB にアクセスがあるテストが失敗する
- ファイルを作成するテストが失敗する

## DB にアクセスがあるテストが失敗する
### 原因(イメージ)
Laravel の`RefreshDatabase`トレイトを使っていると各テストが完了するとテーブルを空にしてくれます。
そのため、複数プロセスでテストを実行すると以下のように、テストが終わったプロセスがまだテスト中のデータを消してしまいテストが失敗します。

1. プロセスA：テストを実行する。テーブルにテスト用データを作る。
1. プロセスB：テストを実行する。同じくテーブルにテスト用データを作る。
1. プロセスA：テストが完了したのでテーブルを空にする。
1. プロセスB：テーブルにあるはずのデータがない！？テスト失敗！ 

### 対処
単一の DB を共有すると上記のような問題が発生するためテスト用 DB を並列実行数分作り、プロセスごとに DB を割り当ててやることで回避しました。
Paratest が用意してくれる環境変数`TEST_TOKEN`はプロセスごとに固有の番号が振られます。
それを参照して接続先の DB を振り分ける以下のメソッドをテストクラスの共通の前処理として挿入しました。

```php
function swapTestingDatabase(): void
    {
        $driver = config('database.default');
        $dbName = config("database.connections.$driver.database");
 
        $testToken = env('TEST_TOKEN', 1);

        $dbNameWithNumber = $testToken == 1
            ? $dbName
            : "{$dbName}-{$testToken}";

        config()->set("database.connections.$driver.database", $dbNameWithNumber);
    }
```
Paratest を使わず通常実行した場合は環境変数`TEST_TOKEN`がないため、その場合はデフォルトのテスト用 DB へ接続されるようになっています。
これは Paratest を使わない単発のテスト実行時や DB を複数作成していない環境でも影響が出ないようにするためです。

### `max_locks_per_transaction`に関するエラーが出る場合
私の環境では DB に PostgreSQL を使っていますがテスト実行時に`max_locks_per_transaction`に関するエラーが出ました。
その場合下記 conf ファイル`/etc/postgresql/11/main/postgresql.conf`の`max_locks_per_transaction`の値を適宜増やしてやることで回避できました。

## ファイルを作成するテストが失敗する
Laravel には`Storage::fake()`というファイルを扱うテストでは便利なメソッドがありますよね。

https://readouble.com/laravel/5.7/ja/mocking.html?header=Storage%2520Fake

このメソッドも先程の DB でのテスト失敗と同じように、別のプロセスがファイルを削除してしまってテストが失敗してしまいます。
これは`Storage::fake()`メソッドが共通のディレクトリを使うようになっているためです。
そこで、メソッドが呼ばれる度に専用のディレクトリを用意するようにメソッドを置き換えました。

```php
function fakeStorage($disk = null)
{
    $disk = $disk ?: $this->app['config']->get('filesystems.default');

    $time = (int) (microtime(true) * 1000);
    $base = storage_path('framework/testing/' . $time);
    $root = $base . '/disks/' . $disk;

    $this->beforeApplicationDestroyed(function () use ($base) {
        (new Filesystem())->deleteDirectory($base);
    });

    return Storage::set($disk, Storage::createLocalDriver(['root' => $root]));
}
```
下記を参考にさせていただきました。

https://medium.com/@richartkeil/laravel-storage-and-parallel-testing-5b8eac44a296

## 実行コマンド
```
./vendor/bin/paratest --processes=8 --runner=WrapperRunner
```

`--processes=<プロセス数>`：並列実行するプロセス数を指定。
`--runner=WrapperRunner`：私の環境ではこのオプションを指定しないと速度が出ませんでした。テストごとにプロセスを作り直すことをしなくなるので早くなるとのことでした。（PHPUnit はテストごとにプロセスを作り直すらしいです）

### 余談
いろいろやって導入したらプロジェクトリーダーが PHP & Laravel のバージョンアップ予算をお客さんからもぎ取ってくれたので短い付き合いになりそうです。やったね！
(上述したように Laravel 8 はデフォルトでテストの並列実行コマンドを用意してくれています)