Help us understand the problem. What is going on with this article?

【PHPUnit】特定のクラス群だけのテストカバレッジを算出する

この記事はうるる 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 を開くとカバレッジが表示されます。
RyCp0.png
(これは適当に拾って来たサンプルです)

カバレッジレポートの出力

今度はカバレッジレポートも出力してみます。

$phpunit --coverage-html "./coverage" --coverage-clover "./clover.xml" tests/

こうすることで、clover.xmlが生成されます。
スクリーンショット 2019-12-03 14.35.27.png
今回は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%なら緑
がつくようにしています。
全部緑ならテンションが上がりますし、気づかぬうちにオレンジや赤になった時でも視覚的に気付きやすくしています。

実際のメッセージはこちらです。
スクリーンショット 2019-12-03 17.55.25.png
スクリーンショット 2019-12-03 17.57.00.png

※色分けをわかりやすくするために、数値は変えています。

まとめ

以上でCircleCIからカバレッジを読み取り、特定のクラス群のカバレッジを計算しSlackへ通知する一連の流れを説明しました。
実際のロジックは部分的に切り取って紹介していますので、実際に実装する際にはアプリケーションロジックに合わせて適宜修正してください。

さらに通知の自動化をさせるために、定期的にバッチなどでこの機能を実行させれば
勝手に計算してSlackで通知してくれるところまでできそうです。

また、カバレッジの推移も取りたい場合はどこかのDBなどに突っ込んだり
それが面倒であればGASを用いてスプレッドシートに突っ込んでもいいかもしれませんね。

ひとまずこれで重要な機能のカバレッジの見える化ができました!

あとがき

Advent Calendar 3日目でした。
明日4日目は Yuuki Noda さんによる記事を乞うご期待!
https://adventar.org/calendars/4548

参考

https://blog.leko.jp/post/how-to-parse-of-coverage-report-with-phpunit/

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした