Laravel の並列テスト+ α で 3 分かかるテストを 30 秒に

Laravel でユニットテストを書けますが、テストケースが増えてくると全ての実行完了までの時間も増えてきます。

今回はテストを並列で実行して全ての実行にかかる時間を短縮してみます。

Laravel の並列テスト

Laravel では v8.25 以降から、並列でテストが実行できるようになっています。

Testing: Getting Started - Laravel - The PHP Framework For Web Artisans

通常 Laravel × PHPUnit では単一プロセス内でテストを順番に実行していくため複数のコアを使用しません。まずはこれを活用してパフォーマンスを上げてみます。

公式ドキュメントにある通り、v10 からは導入するパッケージは brianium/paratest です。

composer require brianium/paratest --dev

導入はこれだけです。

ローカルで並列テスト実行

並列でテストするとどれだけ短縮されるのかを確認してみます。

いくつかのテストを作成済みです。

まずは並列にせず、デフォルトで実行してみます。

./vendor/bin/phpunit
PHPUnit 10.1.3 by Sebastian Bergmann and contributors.

Runtime:       PHP 8.2.6
Configuration: /var/www/html/phpunit.xml

...............................................................  63 / 102 ( 61%)
.......................................                         102 / 102 (100%)

Time: 03:05.672, Memory: 22.00 MB

OK (102 tests, 600302 assertions)

3 分かかりました。できればコーヒーを入れ直してきたら終わっているくらいの時間にはしたいものです。

では並列でテストを実行してみます。artisan コマンド実行時のオプションに --parallel を付与します。

php artisan test --parallel

並列でテストを実行させた結果です。

ParaTest v7.1.4 upon PHPUnit 10.1.3 by Sebastian Bergmann and contributors.

Processes:     4
Runtime:       PHP 8.2.6
Configuration: /var/www/html/phpunit.xml

...............................................................  63 / 102 ( 61%)
.......................................                         102 / 102 (100%)

Time: 01:13.710, Memory: 12.00 MB

OK (102 tests, 600302 assertions)

3 分が 1 分に短縮されました。

実行時の進捗を見ていると、並列実行しない場合は進捗を表すドットが 1 つずつ増えていきますが、並列実行した場合は複数まとめてドットが増えていったので、並列で実行されてるなと見てもとれました。

ちなみにプロセス数が 4 なのはマシンがクアッドコアだからです。マシンスペックに応じて自動でプロセス数が決定されます。

テスト時のプロセス数を制御したい場合はオプション --processes で実現できます。

# プロセス数 2 で実行する
php artisan test --parallel --processes=2

ちなみにコア数より多くのプロセス数を設定してもコア数以上は並列実行できないため効果は上がりません。

試しに 4 コアのマシンでプロセス数 8 にて実行してみましたが、逆に実行時間が 2 分前後に増えました。

ということで、並列実行することでユニットテストの実行時間が短縮できることが確認できました。

GithubActions で並列テスト実行

次に、Github Actions でも並列テストを実行してみます。

プルリクやデプロイ前にユニットテストを実行することはよくあることなので、ここの時間が短縮されたらなおうれしいことです。

ただし、Github Actions で使用されるリソースは決まっていて、Linux 仮想マシンのハードウェアはデフォルトでコア数は 2 です。

GitHub ホステッド ランナーの概要 - GitHub Docs

OS コア 分あたりの料金
Linux 2 $0.008
Linux 4 $0.016

2 コアを 4 コアにすると分あたりの料金は倍になるけど、もし実行時間が半分になるなら同じことですね。(といいつつ、今回は 2 コアだけで試します)

では、Github Actions 上で PHPUnit を実行してみます。まずは並列ではなく、通常の単一プロセスでの実行です。

  • コア数:2
  • プロセス数:1

3 分 37 秒かかりました。

では、並列で実行してみます。

  • コア数:2
  • プロセス数:2

2 分 2 秒でした。1 分 35 秒の短縮なので、およそ 36 % 実行時間が短縮されました。

2 コアとはいえ、3 分の 1 程度時間が削減できるならうれしいですね。

マトリックス戦略で並列テスト実行

ここまでくると複数のジョブに分割して、、もやりたくなります。もはやプロセスがどうとは別の世界線です。

Github Actions には「マトリックス戦略」というものがあって、1 つのジョブを複数のジョブで実行できる仕組みがあります。
ジョブにマトリックスを使用する - GitHub Docs

例えば「同じテスト実行を何パターンかの環境で実施する。OS 違いとかバージョン違いとか。」 みたいなことがこれによって実現できるわけですが、この仕組みを使って実行するテストケース自体を分割して実行してみたらどこまで短縮できるかを試します。

今回は 5 つのジョブを並列実行して、各々で分割したテストを実行してみます。

jobs:
  test:
    name: phpunit
    runs-on: ubuntu-latest

#(略)

    strategy:
      matrix:
        parallel: [ 5 ]
        id: [ 1, 2, 3, 4, 5 ]

#(略)

  steps:
  - name: test
      run: php artisan test --parallel --configuration phpunit_parallel_${{ matrix.id }}.xml

5 つのジョブに対して phpunit.xml も 5 つ作成し、phpunit_parallel_n.xml の中でどのテストを実行するかを定義しておくことで、それぞれのジョブが別々のユニットテストを実行する。という仕掛けです。

# phpunit_parallel_1.xml
<directory suffix="Test.php">./tests/Unit/Dir01</directory>

# phpunit_parallel_2.xml
<directory suffix="Test.php">./tests/Unit/Dir02</directory>

# phpunit_parallel_3.xml
<directory suffix="Test.php">./tests/Unit/Dir03</directory>
.
.
.
  • 並列ジョブ数:5
    • コア数:2
    • プロセス数:2

実行結果です。

26 秒まで短縮されました。

3 分 37 秒から 3 分 11 秒の短縮。1 ジョブ・1 プロセス時の時と比べておよそ 92 % 実行時間が短縮されたことになります。

テストケース自体を分割しているので単純な実行時間が短縮されているのは当然ですが、実際には並列で(=同時に) 5 つのジョブが動いておりそれぞれが 26 〜 30 秒程度で全ての実行が完了しています。 (それぞれのジョブの完了時間に幅が無いのはサンプルでテストが均等に割り振られているから)

最終的に、コーヒーを入れ直しにいく前にテストの実行が終わる。というところまで短縮されました。

まとめ

Laravel の並列テストは導入が簡単で得られる恩恵もなかなか大きかったです。

PHPUnit に限らずユニットテストなどの自動テストは、工夫次第で実行時間の短縮を図れることもわかりました。

時間は有限なので、不要な待ち時間はどんどん削減していきたいですね。


現在 back check 開発チームでは一緒に働く仲間を募集中です。 herp.careers herp.careers herp.careers