この記事はうるる Advent Calendar 2019 3日目の記事です。
はじめに
今回はPHPUnitにおけるカバレッジを、特定のクラスにだけ絞って算出する方法を紹介いたします。
テストカバレッジは、一般的に高ければいいというものでもなければ、低くてもいいというものではありません。
そのサービスにおいて最適な品質保証として、どの程度テストを網羅していればいいかどうかは、サービスにより異なります。
もちろんカバレッジを100%にしてあるに越したことはないですが
開発コストなどを考えると、どうでもいい機能はテストを書きたくないし、その代わりに重要な機能に対してはちゃんとテストコードを書きたい
なんてこともあると思います。
この重要な機能に対してのみのカバレッジを計測したいという思いから
特定のクラス群のカバレッジを計測しましたので、その紹介をしたいと思います。
システム構成
今回使用するアプリケーションは
Laravel5.5を使用しています。
PHPは7.0、PHPUnitは6.5.14です。
通常のCoverage出力
PHPUnitによる一般的なカバレッジ出力は以下です。
$phpunit --coverage-html "./coverage" tests/
この結果から
./coverage
が生成され、./coverage/index.html
を開くとカバレッジが表示されます。
(これは適当に拾って来たサンプルです)
カバレッジレポートの出力
今度はカバレッジレポートも出力してみます。
$phpunit --coverage-html "./coverage" --coverage-clover "./clover.xml" tests/
こうすることで、clover.xml
が生成されます。
今回はCircleCI上で実行していたので、パスにcircleci
と書いてありますがあまり気にしないでください。
こちらのカバレッジレポートには、どのクラスがどれくらいの大きさで、どれくらいカバレッジしているかという数値が細かく入っています。
そこから一部抜粋し、計算してみます。
<file name="/home/circleci/app/app/Http/Controllers/Admin/UserController.php">
<class name="App\Http\Controllers\Admin\UserController" namespace="App\Http\Controllers\Admin">
<metrics complexity="7" methods="5" coveredmethods="4" conditionals="0" coveredconditionals="0" statements="25" coveredstatements="22" elements="30" coveredelements="26"/>
</class>
<line num="18" type="method" name="__construct" visibility="public" complexity="1" crap="1" count="1"/>
<line num="20" type="stmt" count="1"/>
...略
<line num="63" type="stmt" count="0"/>
<metrics loc="65" ncloc="62" classes="1" methods="5" coveredmethods="4" conditionals="0" coveredconditionals="0" statements="27" coveredstatements="23" elements="32" coveredelements="27"/>
</file>
↓ file > metrics
のパラメーターを参照
metrics | count | covered | coverage |
---|---|---|---|
methods | 5 | 4 | 4/5 = 80% |
statements(lines) | 27 | 23 | 23/27 = 85.16% |
つまり、このUserController
のカバレッジはlineベースで85%ほどあるというわけです。
最初にHTMLで出力したものは、このカバレッジレポート値をいい感じにUIに変換したようなイメージで考えると良いでしょう。
特定クラス群のCoverage計算
さてここからが本題です。
PHPUnitでカバレッジを出力すると、必ず全体のLine数やメソッド数をベースにして計算してしまうため
本来押さえておくべきクラスやメソッドのみのカバレッジ(割合)を算出することができません。
そのため、phpを用いて上記のカバレッジレポート(clover.xml)から、本当に欲しい部分のみのカバレッジを算出できる機能を実装します。
クラスの指定
phpに定数配列で直接指定してしまいます。
ここでは、clover.xmlに出力されるnamespace
をそのまま指定してあげます。
(指定したクラスのみを取り出して計算するため、検索トリガーにnamespaceを使います)
const TARGET_CLASSES = [
'App\Http\Controllers\UserController',
'App\Http\Controllers\PurcahseController',
'App\Http\Controllers\LoginController',
];
xmlの読み込み
$xml = simplexml_load_file('/clover.xml'); // 適切なパスを指定
集計
$coverages = [];
$totalCoverage = [
'title' => '【Total】',
'methods' => 0,
'coveredmethods' => 0,
'statements' => 0,
'coveredstatements' => 0
];
foreach ($xml->project->package as $package) {
foreach ($package->file as $file) {
$class = (string)$file->class['name'];
if (in_array($class, self::TARGET_CLASSES)) {
// 設定したクラスに対して、カバレッジを取得する
// クラスごとにメソッド数ライン数を配列に入れ込んでいく
$coverages[$class] = [
'title' => '【' . $class . '】',
'methods' => (integer)$file->metrics['methods'],
'coveredmethods' => (integer)$file->metrics['coveredmethods'],
'statements' => (integer)$file->metrics['statements'],
'coveredstatements' => (integer)$file->metrics['coveredstatements']
];
// 各クラスのカバレッジ計測
$coverages[$class]['MethodCoverage'] = round(($coverages[$class]['coveredmethods']/$coverages[$class]['methods'])*100, 2);
$coverages[$class]['StatementsCoverage'] = round(($coverages[$class]['coveredstatements']/$coverages[$class]['statements'])*100, 2);
// 全体のカバレッジ集計
$totalCoverage['methods'] += $coverages[$class]['methods'];
$totalCoverage['coveredmethods'] += $coverages[$class]['coveredmethods'];
$totalCoverage['statements'] += $coverages[$class]['statements'];
$totalCoverage['coveredstatements'] += $coverages[$class]['coveredstatements'];
}
}
}
// 各クラスの集計が終わったところでトータルのカバレッジを計算
$totalCoverage['MethodCoverage'] = round(($totalCoverage['coveredmethods']/$totalCoverage['methods'])*100, 2);
$totalCoverage['StatementsCoverage'] = round(($totalCoverage['coveredstatements']/$totalCoverage['statements'])*100, 2);
var_dump($totalCoverage);
array(7) {
'title' => string(11) "【Total】"
'methods' => int(95)
'coveredmethods' => int(91)
'statements' => int(1190)
'coveredstatements' => int(1173)
'MethodCoverage' => double(95.79)
'StatementsCoverage' => double(98.57)
}
これで、本当に計測したいクラスだけを集めたカバレッジを計測することができました。
おまけ1:CircleCIからカバレッジレポートを取得
私が担当しているサービスでは、前提のテストカバレッジを毎晩夜中にCiecleCIで計測しています。
全体のカバレッジは、CircleCIで出力したhtmlを開くことで参照ができますが
上記のロジックを用いて、特定のクラス群のみのカバレッジを計測する方法をご紹介します。
方法はいたってシンプルで、最初に読み込むxmlの参照先を変えるだけです。
$url = 'https://circleci.com/api/v1.1/project/github/:username/:project/latest/artifacts';
$token = $this->circleCiToken;
$branch = 'develop';
$filter = 'completed';
$artifacts = $this->execCurlCommand($url . '?' . http_build_query(['circle-token' => $token, 'branch' => $branch, 'filter' => $filter]));
$clover = Arr::first($artifacts, function ($file) {
return $file['path'] === 'build/logs/clover.xml';
});
$xml = simplexml_load_file($clover['url'].'?circle-token='.$this->circleCiToken);
こちらはCircleCIのAPIを参考に実装しています。
https://circleci.com/docs/api/#artifacts-of-the-latest-build
:vcs-type
= github
:username/
= CircleCIのユーザーネーム
:project
= repogitory名
を入れます。
APITokenは、ドキュメントを参考に生成します。
https://circleci.com/docs/api/#add-an-api-token
artifacts-of-the-latest-build
を実行するとArtifactの中身が全て返って来ます。
var_dump($artifacts);
array(902) {
[0] =>
array(4) {
'path' =>
string(17) "phpunit/junit.xml"
'pretty_path' =>
string(17) "phpunit/junit.xml"
'node_index' =>
int(0)
'url' =>
string(65) "https://1111-11111111-gh.circle-artifacts.com/0/phpunit/junit.xml"
}
[1] =>
array(4) {
'path' =>
string(24) "schemaspy/anomalies.html"
'pretty_path' =>
string(24) "schemaspy/anomalies.html"
'node_index' =>
int(0)
'url' =>
string(72) "https://1111-11111111-gh.circle-artifacts.com/0/schemaspy/anomalies.html"
}
...略
array(4) {
'path' =>
string(21) "build/logs/clover.xml"
'pretty_path' =>
string(21) "build/logs/clover.xml"
'node_index' =>
int(0)
'url' =>
string(69) "https://1111-11111111-gh.circle-artifacts.com/0/build/logs/clover.xml"
}
以下省略
ここから、clover.xml
へアクセスするためのurlを取得することで、xmlを取得することが可能です。
おまけ2:Slackへカバレッジを通知
最後に計測したカバレッジをSlackへ通知します。
今回はLaravelを使ってい実装しているため、Slack通知自体はLaravelの基本機能を使用します。
https://readouble.com/laravel/5.5/ja/notifications.html
実際に通知を送るロジックは以下です。
// 各クラスのカバレッジを1つずつSlackへ通知する
collect($coverages)->each(function ($coverage) {
$slackMessage = (new SlackMessage)
->from($this->name)
->to($this->channel);
$this->setAtachmentType($slackMessage, $coverage['MethodCoverage']);
$slackMessage->attachment(function ($attachment) {
$attachment
->fallback($this->content['title'])
->title($this->content['title'])
->fields($this->content['message']);
});
});
ここでのポイントは、setAtachmentType
メソッドで、slackで表示されるメッセージに色をつけている部分です。
private function setAtachmentType(&$slack, $coverageRate)
{
switch (true) {
case ($coverageRate >= 80):
return self::SUCCESS;
case ($coverageRate >= 50):
return self::WARNING;
default:
return self::ERROR;
}
switch ($this->type) {
case self::ERROR:
$slack->error();
break;
case self::WARNING:
$slack->warning();
break;
default:
$slack->success();
break;
}
}
カバレッジの数値を判定し、メッセージへ色をつけています。
0~49%なら赤
50~79%ならオレンジ
80~100%なら緑
がつくようにしています。
全部緑ならテンションが上がりますし、気づかぬうちにオレンジや赤になった時でも視覚的に気付きやすくしています。
※色分けをわかりやすくするために、数値は変えています。
まとめ
以上でCircleCIからカバレッジを読み取り、特定のクラス群のカバレッジを計算しSlackへ通知する一連の流れを説明しました。
実際のロジックは部分的に切り取って紹介していますので、実際に実装する際にはアプリケーションロジックに合わせて適宜修正してください。
さらに通知の自動化をさせるために、定期的にバッチなどでこの機能を実行させれば
勝手に計算してSlackで通知してくれるところまでできそうです。
また、カバレッジの推移も取りたい場合はどこかのDBなどに突っ込んだり
それが面倒であればGASを用いてスプレッドシートに突っ込んでもいいかもしれませんね。
ひとまずこれで重要な機能のカバレッジの見える化ができました!
あとがき
Advent Calendar 3日目でした。
明日4日目は Yuuki Noda さんによる記事を乞うご期待!
https://adventar.org/calendars/4548
参考