PHPUnitを並列実行してできたJUnit XMLファイルとカバレッジファイルをマージする方法

グレンジでサーバー・インフラを担当している村田です。

[グレンジ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/grenge)の1日目の記事です。

グレンジでPHPUnitを使ってテストを行っていますが、
テストが増えるにつれてテストに時間がかかることが問題になりました。

テストをテストファイル単位やディレクトリ単位などで分け、PHPUnitを並列実行すればそれだけ早く終わらせることができます。
しかし、その結果ファイルのマージはPHPUnit単体では不可能です。

テスト結果のマージする方法をまとめた記事になります。

# 前提条件
JUnit XMLファイルとカバレッジファイルを.build/junitと.build/phpcovに出力していることを前提としています。

```.sh
% cd /home/username/src/tests/
% phpunit a_test.php                                     \
   --log-junit=/home/username/src/.build/junit/a.xml     \
   --coverage-php=/home/username/src/.build/phpcov/a.cov
% phpunit b_test.php                                     \
   --log-junit=/home/username/src/.build/junit/b.xml     \
   --coverage-php=/home/username/src/.build/phpcov/b.cov
```

```.sh
% tree /home/username/src/.build
/home/username/src/.build
├── junit
│   ├── a.xml
│   └── b.xml
└── phpcov
    ├── a.cov
    └── b.cov
```

# JUnit XMLファイルのマージ

PHPUnit作者のSebastian Bergmannさんがmerge-phpunit-xml.phpという、JUnit XMLファイルをマージするスクリプトを公開しています。
https://gist.github.com/sebastianbergmann/4405658

しかし、このコードは

- マージするとテスト結果が重複される
- ライブラリをrequireしていないのでこのままでは実行できない

という優しくない状態です。
なので、結果を重複しないように修正して、ライブラリはcomposerで取得できるように手を加えたGitHubリポジトリがこちら。
https://github.com/muratahiroshi/merge-phpunit-xml

そのDockerイメージも作成。
https://hub.docker.com/r/hiroshimurata/merge-phpunit-xml

次のように実行すると.build/junitのJUnit XMLファイルたちがマージされて、junit_merge.xmlが出来上がります。

```.sh
% docker run -v /home/username/src:/data \
   hiroshimurata/merge-phpunit-xml       \
   /data/.build/junit                    \
   /data/junit_merge.xml
```

# カバレッジファイルをマージしてCloverとHTMLで書き出す
PHPのカバレッジファイルはphpcovでマージできます。これもSebastian氏作。
https://github.com/sebastianbergmann/phpcov

Pharでも配布されていますが、GCPのCloud Buildでさくっと使えるようにこれもDockerイメージ作成。
https://hub.docker.com/r/hiroshimurata/phpcov

ただし、Dockerで実行するときは要注意。
phpcovのマージ処理はソースコードを参照するので、Dockerから参照できるようマウントしてあげます。
また、Dockerにマウントするソースコードのパスは、PHPUnit実行時と同じパスにしないといけません。

前提条件のパスに合わせて、Dockerのphpcovを実行する例。Clover形式のXMLやHTMLで書き出せたりします。

```.sh
% docker run -v /home/username/src:/home/username/src hiroshimurata/phpcov:6.0.0 \
   merge /home/username/src/.build/phpcov/                                       \
   --clover /home/username/src/.build/clover.xml                                 \
   --html /home/username/src/.build/html
```

できたindex.htmlに何一つ結果が表示されていないようなら、ソースコードの参照、パスの位置関係に失敗しています。
covファイルの中身を見るとパスを確認できるので、それに合わせると良いです。

```.php
<?php
$coverage = new SebastianBergmann\CodeCoverage\CodeCoverage;
$coverage->setData(array (
  '/home/username/src/app/library/A.php' => # このパス
  array (
```

Cloud BuildでPHPUnitの実行からphpcovまでまとめて行うなら、パスの問題は生じないと思います。

# まとめ

一時期、2時間近くかかっていた全テストも並列分散させて数十分に抑えることができました。
CIは手早く回せるだけ快適＆緊急対応時の変な汗も減らせて快適。

# おまけ boxで作るPhar

Dockerの[merge-phpunit-xml](https://github.com/muratahiroshi/merge-phpunit-xml)はboxを使ってPharを作成しています。

```Dockerfile
FROM composer:1.8.5
WORKDIR /tmp/merge-phpunit-xml
ADD merge-phpunit-xml.php composer.json composer.lock box.json /tmp/merge-phpunit-xml/
RUN /usr/bin/composer install --no-dev
ADD https://github.com/humbug/box/releases/download/3.7.0/box.phar /usr/bin/box
RUN php /usr/bin/box compile

FROM php:7.2.15
COPY --from=0 /tmp/merge-phpunit-xml/merge-phpunit-xml.phar /usr/bin/merge-phpunit-xml
ENTRYPOINT ["/usr/bin/merge-phpunit-xml"]
```

box.jsonを書いてコンパイルすれば、1ファイルにまとめられるのでスッキリ。

```box.json
{
  "alias": "merge-phpunit-xml.phar",
  "files": [
    "merge-phpunit-xml.php",
    "vendor/autoload.php"
  ],
  "main": "merge-phpunit-xml.php",
  "output": "merge-phpunit-xml.phar",
  "compression": "GZ"
}
```

# とても重要なおまけ告知

本の執筆に参加しました。
[ゲーム開発が変わる！GCPゲームインフラ実践ガイド (NextPublishing)](https://www.amazon.co.jp/dp/4844378325)