Laravelの並列テスト(ParaTest)の巻

Date:

Share post:

また、しばらくぶりの投稿になってしまいました・・・。

今回はLaravelネタになります。環境は以下の通りです。

OS:Ubuntu 22.04 LTS
PHP:8.1
MariaDB:10.6.22
Laravel:10.50.2
Vagrant + VMware Fusion(ローカル環境)

Laravelで開発したシステムのテストを行っていたのですが、フルテスト件数が900件を超えていて、カバレッジ込みで単一プロセス実行したときに、実測で13分前後かかってしまう状況でした。うーん、時間がかかる・・・。

なんとか時短できる方法がないものかとAIに聞いて調べてみたところ、ParaTestを使用した並列テストができるということを知りましたので、どれくらい短くなるものか試してみました。

まずは必要なものを準備します。

1. Composer パッケージ

以下のパッケージが必要となるので、入れました。

パッケージ        バージョン         役割
brianium/paratest    ^7.3(PHP8.1のため)    PHPUnit の並列ランナー
phpunit/phpunit     10.5.60 → 10.5.31    PHPUnit 本体(バージョン固定については後述)

ParaTest 7.4 は今回の環境であるPHP8.1では動作しないため、ParaTest 7.3 としています。

composer.json(初回エラー時の抜粋):

"require-dev": {
    "brianium/paratest": "^7.3",
    "phpunit/phpunit": "10.5.60",
    ...
}

2. composerコマンドの実行

以下コマンドを実行してパッケージをインストールしました。

$ composer require --dev brianium/paratest:^7.3
    ...
Call to undefined method
PHPUnit\TestRunner\TestResult\TestResult::wasSuccessfulIgnoringPhpunitWarnings()

ガーン、エラーでした。原因をCursorに調べてもらうと、

      • Composer が phpunit/phpunit を 10.5.60 に解決した

      • ParaTest 7.3 側が想定している PHPUnit の API とずれる

とバージョン不整合でうまく入らないようです。そのため、当初指定したPHPUnit のバージョンを

composer.json(成功時の抜粋):

"require-dev": {
    "brianium/paratest": "^7.3",
    "phpunit/phpunit": "10.5.31",
    ...
}

と固定にしたという次第です。変更した後に

$ composer update phpunit/phpunit --with-all-dependencies

とすることで、無事にパッケージの導入ができました。

3. 並列テスト用のDBの準備

Laravelで並列テストする際は、ワーカーごとに別DBを使用することになります。例えばプロセス数が2(後述するコマンドのオプションだと –processes=2 )の場合、_test_1 と _test_2 を作成する必要があります。

      • ベースのDB: .env のDB_DATABASE (仮に testingとします。)
      • ワーカー用: {DB_DATABASE}_test_{N} となるので、作成するDBは testing_test_1, testing_test_2
      • 作成したDBの権限:アプリ用のデータベースユーザー(仮に testing )に GRANT

以下のようなSQLコマンドを実行して、DBの作成と権限付与を行います。以下の SQL は、MariaDB / MySQL いずれでも同様です。root など権限のあるユーザーで実行し、アプリ用のデータベースユーザーに並列用DBへの権限を付与します。

CREATE DATABASE IF NOT EXISTS `testing_test_1` ...;
CREATE DATABASE IF NOT EXISTS `testing_test_2` ...;
GRANT ALL PRIVILEGES ON `testing_test_1`.* TO 'testing'@'localhost';
GRANT ALL PRIVILEGES ON `testing_test_2`.* TO 'testing'@'localhost';
FLUSH PRIVILEGES;

4. 並列テストの実行

ようやく準備が整いましたので、コマンド実行です。と思いましたが、その前に従来の単一プロセス実行の場合を参考で添付します。

画像の最後の方に出ている通り、13分5秒かかっています。

それでは並列実行です。以下コマンドです。

$ php artisan test --env=testing --parallel --processes=2 --coverage (カバレッジ込みの場合)

7分30秒 と単一の時の実行時間の 57% くらいに短縮されています。

プロセス数を増やせばもっと時短できるかもと思いますが、今回のようなローカル環境での実行の場合は、2プロセス程度が適当なようです。

あと、並列実行のテストの注意点としては、今回の場合だと2プロセスが並行でテストを実行するので、ファイルシステムやグローバル状態、同一DB行を共有しないようにテストを作成する必要があります。実際に単一プロセスでは通っていたのに、並列にしたらだけ失敗した例が今回は2つありました。

1つ目は、テスト専用のスタブ(フェイク)クラスを、テストクラスと同じPHPファイルの末尾にだけ書いていたケースです。単一実行ではそのファイルを読み込めばクラスが存在するのですが、並列ではワーカーごとにテストの分割・読み込み順が変わるため、定義しているファイルがロードされないことがあり、「クラスが見つからない」というエラーになりました。対応としては、そのクラスを別ファイルに分け、PSR-4のオートロードで確実に読み込めるようにしました。

2つ目は、ログのクリーンアップコマンドのテストで、「ログディレクトリにファイルがないときの表示」を検証するために、実際のログディレクトリ上のファイルを一時的に退避したり戻したりしていたケースです。複数ワーカーが同じディレクトリを触ると、rename や glob のタイミングがぶつかり、並列実行時にだけ、再現性のない失敗が起きました。対応としては、ログファイル一覧が常に空になるテスト用のコマンドを用意し、実ファイルを触らずにメッセージだけを検証する形に変えました。

いずれも「プロセスが分かれた」ことで初めて表面化したので、並列化はコマンドやDBの準備だけでなく、テストコードの見直しもセットで考えた方がよさそうです。

今回の場合、並列実行を行うことで全件・カバレッジ込みでは 13分05秒 から 7分30秒 まで短くできました。2プロセスでも実行時間は半分には届きませんが、マージ前に全件を回す場面では、待ち時間の差はかなり感じられます。PHPUnit は ParaTest と合う版に固定しておくこと、並列用DBの作成と権限、テストが共有リソースを触っていないか、を押さえておけば、同じような環境でも再現しやすいと思います。
 
日常の開発では変更した箇所だけテストし、全件は並列でまとめて確認する、という使い分けにしています。テストの実行時間に悩んでいる方の参考になれば幸いです。

Related articles