CircleCI のテストの並列実行は、テストファイルをインスタンス毎に振り分けることで実現します。しかしながら、PHPUnit のコマンドライン (phpunit
) は複数のファイル名を受け取って実行するということができません。
circleci tests glob "tests/**/*Test.php" | circleci tests split
# => インスタンスに割り当てられたテストファイル名のリスト (スペース区切)
# => そのままでは phpunit で実行できない...
そこで、ファイル名のリストを引数にとって、そのテストを実行するための phpunit.xml
(phpunit-partial.xml
) を生成するツールを書いて対応しました。
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
) オプションに渡してやれば、任意のテストファイルだけ実行できます。
<?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 の設定
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 # ← これ
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 はこんな感じ。
workflows:
version: 2
build-test-deploy:
jobs:
- build
- phpunit:
requires:
- build
- report:
requires:
- phpunit