概要
Paratest という PHPUnit を並列(複数プロセスで同時)実行してくれるツールで自動テストを並列実行してみたところ、自動テストの実行速度が最大で半分くらいになりました。
毎回10分以上かかっていたのが5分台まで短くなりテスト気軽に流せるようになりました。
ただ、私のプロジェクトの環境では導入時に少し工夫が必要だったためその点について記載します。
その前に:Laravel 8 以降を使っている方へ
テストを並列実行できる機能がデフォルトであるので下記をどうぞ!
環境
- PHP 7.3
- Laravel 5.8
- PHPUnit 7.5
- Paratest 3.1.2
- PostgreSQL 11.6
※ Paratest のバージョンが古いのはプロジェクトで使っている PHPUnit のバージョンに合わせたためです。
課題
Paratest を導入しても何も設定を変えずに実行はできませんでした。
大きく2つ課題があったのでその対処法について説明します。
- DB にアクセスがあるテストが失敗する
- ファイルを作成するテストが失敗する
DB にアクセスがあるテストが失敗する
原因(イメージ)
Laravel のRefreshDatabase
トレイトを使っていると各テストが完了するとテーブルを空にしてくれます。
そのため、複数プロセスでテストを実行すると以下のように、テストが終わったプロセスがまだテスト中のデータを消してしまいテストが失敗します。
- プロセスA:テストを実行する。テーブルにテスト用データを作る。
- プロセスB:テストを実行する。同じくテーブルにテスト用データを作る。
- プロセスA:テストが完了したのでテーブルを空にする。
- プロセスB:テーブルにあるはずのデータがない!?テスト失敗!
対処
単一の DB を共有すると上記のような問題が発生するためテスト用 DB を並列実行数分作り、プロセスごとに DB を割り当ててやることで回避しました。
Paratest が用意してくれる環境変数TEST_TOKEN
はプロセスごとに固有の番号が振られます。
それを参照して接続先の DB を振り分ける以下のメソッドをテストクラスの共通の前処理として挿入しました。
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()
というファイルを扱うテストでは便利なメソッドがありますよね。
このメソッドも先程の DB でのテスト失敗と同じように、別のプロセスがファイルを削除してしまってテストが失敗してしまいます。
これはStorage::fake()
メソッドが共通のディレクトリを使うようになっているためです。
そこで、メソッドが呼ばれる度に専用のディレクトリを用意するようにメソッドを置き換えました。
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]));
}
下記を参考にさせていただきました。
実行コマンド
./vendor/bin/paratest --processes=8 --runner=WrapperRunner
--processes=<プロセス数>
:並列実行するプロセス数を指定。
--runner=WrapperRunner
:私の環境ではこのオプションを指定しないと速度が出ませんでした。テストごとにプロセスを作り直すことをしなくなるので早くなるとのことでした。(PHPUnit はテストごとにプロセスを作り直すらしいです)
余談
いろいろやって導入したらプロジェクトリーダーが PHP & Laravel のバージョンアップ予算をお客さんからもぎ取ってくれたので短い付き合いになりそうです。やったね!
(上述したように Laravel 8 はデフォルトでテストの並列実行コマンドを用意してくれています)