CircleCI で PHPUnit を並列実行する


CircleCI のテストの並列実行は、テストファイルをインスタンス毎に振り分けることで実現します。しかしながら、PHPUnit のコマンドライン (phpunit) は複数のファイル名を受け取って実行するということができません。

circleci tests glob "tests/**/*Test.php" | circleci tests split
# => インスタンスに割り当てられたテストファイル名のリスト (スペース区切)
# => そのままでは phpunit で実行できない...

そこで、ファイル名のリストを引数にとって、そのテストを実行するための phpunit.xml (phpunit-partial.xml) を生成するツールを書いて対応しました。

phpunit-xml-gen.php

phpunit-xml-gen.php
<?php

$files = array_slice($argv, 1);

$xml = new DOMDocument();
$xml->load(__DIR__ . '/phpunit.xml');

$testsuite = $xml->createElement('testsuite');
$testsuite->setAttribute('name', 'partial');

foreach ($files as $file) {
    $testsuite->appendChild($xml->createElement('file', $file));
}

$testsuites = $xml->createElement('testsuites');
$testsuites->appendChild($testsuite);

$phpunit = $xml->getElementsByTagName('phpunit')->item(0);
$phpunit->replaceChild($testsuites, $phpunit->getElementsByTagName('testsuites')->item(0));

$xml->save(__DIR__ . '/phpunit-partial.xml');

exit(0);
  • 同じディレクトリ内にある phpunit.xml<testsuites /> 要素内を、渡されたファイル名を実行する <testsuite /> に置き換えて、phpunit-partial.xml を生成する。
  • プロジェクトの phpunit.xml から生成するので、実行ファイル以外の諸々の設定を引き継げる。
  • そのまま phpunit を実行するスクリプトにしても良かったが、実行時にオプションを渡したいのでそれはやめた。

これを以下のような感じで実行すると(要 phpunit.xml)、

$ echo tests/FooTest.php tests/BarTest.php | xargs php ./phpunit-xml-gen.php

こんな感じの phpunit-partial.xml が生成されます。このファイルを phpunit-c (--configuration) オプションに渡してやれば、任意のテストファイルだけ実行できます。

phpunit-partial.xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="partial">
            <file>tests/FooTest.php</file>
            <file>tests/BarTest.php</file>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./app</directory>
        </whitelist>
    </filter>
    <php>
        <env name="APP_ENV" value="testing"/>
    </php>
</phpunit>

CircleCI の設定

.circleci/config.yml
jobs:

  phpunit:
    parallelism: 4
    steps:
      - run:
          command: |
            circleci tests glob "tests/Feature/**/*Test.php" "tests/Unit/**/*Test.php" | circleci tests split --split-by=timings | xargs php ./phpunit-xml-gen.php
            php ./vendor/bin/phpunit \
              --verbose \
              --log-junit tmp/test-reports/phpunit/logfile.xml \
              --coverage-clover tmp/coverage-${CIRCLE_NODE_INDEX}.xml \
              --configuration phpunit-partial.xml
      - store_test_results:
          path: tmp/test-reports
      - persist_to_workspace:
          root: /var/www/html
          paths:
            - tmp/coverage-*.xml

store_test_results

JUnit XML 形式のファイルを CircleCI に保存するように設定すると、テストファイルの実行時間から良い感じに各インスタンスに振り分けてくれるようになるそうです。設定できると、上記のように Test Summary が表示されるようになります。

PHPUnit の場合は、--log-junit オプションで生成できるので、それを store_test_results で保存するようにした上で、テスト振り分けのコマンドの引数に --split-by=timings を渡してやります。

php ./vendor/bin/phpunit \
  --log-junit tmp/test-reports/phpunit/logfile.xml # ← これ
circleci tests split --split-by=timings

カバレッジ

テストを実行したインスタンス内で生成されるカバレッジの結果は、そのインスタンスで実行したテストの分しか反映されません。

Coveralls などのサービスを利用する場合、すべてのインスタンスのテストの実行が完了するのを待って、別の job で各インスタンスのカバレッジをまとめて送ってやる必要があります。

PHPUnit の場合は、phpunit の実行時に生成するカバレッジのファイル名をインスタンス毎に分かれるようにして、persist_to_workspace / attach_workspace で良い感じに集めてやります。

php ./vendor/bin/phpunit \
  --coverage-clover tmp/coverage-${CIRCLE_NODE_INDEX}.xml # ← これ
.circleci/config.yml
jobs:

  report:
    steps:
      - attach_workspace:
          at: /var/www/html
      - run:
          # NOTE: require $COVERALLS_REPO_TOKEN
          command: |
            php ./vendor/bin/php-coveralls \
              --verbose \
              --json_path=tmp/coveralls-upload.json \
              --coverage_clover=tmp/coverage-*.xml

workflow はこんな感じ。

.circleci/config.yml
workflows:
  version: 2
  build-test-deploy:
    jobs:
      - build
      - phpunit:
          requires:
            - build
      - report:
          requires:
            - phpunit

参考