CircleCI で PHPUnit を並列実行する

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

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

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

# phpunit-xml-gen.php

```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`）、

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

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

```xml: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 の設定

```yaml:.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


<img width="850" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/16100/101454bd-8f6f-0f11-2154-587119ad5a2a.png">

https://circleci.com/docs/2.0/configuration-reference/#store_test_results

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

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

```sh
php ./vendor/bin/phpunit \
  --log-junit tmp/test-reports/phpunit/logfile.xml # ← これ
```

```sh
circleci tests split --split-by=timings
```

## カバレッジ

<img width="716" alt="" src="https://qiita-image-store.s3.amazonaws.com/0/16100/ace8426f-812c-90d2-0ad5-b80b303a1c87.png">

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

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

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

```sh
php ./vendor/bin/phpunit \
  --coverage-clover tmp/coverage-${CIRCLE_NODE_INDEX}.xml # ← これ
```

```yaml:.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 はこんな感じ。

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

# 参考

- https://circleci.com/docs/2.0/parallelism-faster-jobs/
- https://phpunit.readthedocs.io/en/7.3/textui.html
- https://phpunit.readthedocs.io/en/7.3/configuration.html