第11章 コードカバレッジ解析

 

In computer science, code coverage is a measure used to describe the degree to which the source code of a program is tested by a particular test suite. A program with high code coverage has been more thoroughly tested and has a lower chance of containing software bugs than a program with low code coverage.

 
 --Wikipedia

この章では、PHPUnit のコードカバレッジ機能について学びます。 これは、テストを実行したときに、実装コードのどの部分が実行されたかを調べるものです。 PHPUnit のコードカバレッジ解析では PHP_CodeCoverage コンポーネントを使っています。このコンポーネントは、 Xdebug 拡張モジュールが提供するステートメントカバレッジ機能を利用しています。

注記

Xdebug は PHPUnit 本体には組み込まれていません。 テストを実行したときに Xdebug がロードできないという notice が出る場合は、 Xdebug がインストールされていないかあるいはうまく設定できていないのでしょう。 PHPUnit のコードカバレッジ機能を使う前に、まずは Xdebug のインストールガイド を読んでみましょう。

PHPUnit は、HTML ベースのコードカバレッジレポートを生成するだけでなく、 XML ベースのログファイルにコードカバレッジ情報を出力することもできます。 Clover、Crap4J、PHPUnit など、さまざまな形式に対応しています。 また、コードカバレッジ情報をテキスト形式で出力 (そして、標準出力に表示) したり、PHP のコードとして出力して後処理をしたりすることもできます。

コードカバレッジ機能を制御するための コマンドラインスイッチの一覧は、第 3 章 を参照ください。 また、設定項目については 「ログ出力」 を参照ください。

コードカバレッジの指標

コードカバレッジを計測するための指標には、さまざまなものがあります。

Line Coverage

ラインカバレッジ は、 実行可能な行が実行されたかどうかを計測します。

Function and Method Coverage

関数・メソッドカバレッジ は、 関数やメソッドが実行されたかどうかを計測します。 PHP_CodeCoverage は、その関数やメソッド内の実行可能な行がすべて実行された場合にのみ、 その関数やメソッドが実行されたとみなします。

クラス・トレイトカバレッジ

クラス・トレイトカバレッジ は、 クラスやトレイトがカバーされたかどうかを計測します。 PHP_CodeCoverage は、クラスやトレイト内のすべてのメソッドがカバーされている場合にのみ、 そのクラスやトレイトがカバーされたとみなします。

Opcode Coverage

オペコードカバレッジ は、関数やメソッドのオペコードが、 テストスイートの実行中に実行されたかどうかを計測します。 通常は、1 行のコードをコンパイルすると、複数のオペコードになります。 ラインカバレッジは、複数のオペコードのうち少なくともひとつが実行された時点で、 その行が実行されたとみなします。

Branch Coverage

ブランチカバレッジ は、テストスイートの実行中に、 制御構造内の boolean 式が true あるいは false のどちらかとして評価されたかどうかを計測します。

Path Coverage

パスカバレッジ は、テストスイートの実行中に、 関数やメソッド内で取りうる実行パスが網羅されたかどうかを計測します。 実行パスとは、関数やメソッドに入ってから出るまでの間のルート内での分岐のことです。

Change Risk Anti-Patterns (CRAP) Index

Change Risk Anti-Patterns (CRAP) インデックス とは、循環的複雑度と、あるコード単位のコードカバレッジに基づいて算出される指標です。 複雑度が低く、適切なテストカバレッジが達成されているコードは、CRAPインデックスの値が低くなります。 CRAPインデックスを下げるには、テストを書くか、 あるいはリファクタリングでコードの複雑性を下げます。

注記

オペコードカバレッジブランチカバレッジパスカバレッジ については、 PHP_CodeCoverage ではまだサポートしていません。

ファイルのインクルードや除外

デフォルトでは、1 行でもコードが実行されたソースコードファイルはすべて (そしてそのようなファイルのみが) コードカバレッジレポートに含められます。

デフォルトでは、ブラックリストを使って、 コードカバレッジレポートから除外するファイルを指定します。 ブラックリストには、あらかじめ、PHPUnit や依存ライブラリのファイルが指定されています。

このブラックリストではなく、ホワイトリストを使うことをおすすめします。

オプションで、ホワイトリストに追加したファイルをすべて、コードカバレッジレポートに追加することもできます。 そのためには、PHPUnit の設定で addUncoveredFilesFromWhitelist="true" とします (「コードカバレッジ対象のファイルの追加や除外」 を参照ください)。 こうすれば、まだテストされていないファイルもすべて、レポートに含めることができます。 カバーされていないファイルにおける、実行可能な行についての情報を知りたい場合は、同じく PHPUnit の設定で processUncoveredFilesFromWhitelist="true" とします (「コードカバレッジ対象のファイルの追加や除外」 を参照ください)。

注記

processUncoveredFilesFromWhitelist="true" が設定されている場合のソースコードファイルの読み込みでは、 もしクラスや関数のスコープから外れるコードが含まれていたときに問題が起こる可能性があります。

コードブロックの無視

どうしてもテストができないコードブロックなどを、 コードカバレッジ解析時に無視させたいこともあるでしょう。 PHPUnit でこれを実現するには、 @codeCoverageIgnore@codeCoverageIgnoreStart および @codeCoverageIgnoreEnd アノテーションを 例 11.1 のように使用します。

例 11.1: @codeCoverageIgnore@codeCoverageIgnoreStart および @codeCoverageIgnoreEnd アノテーションの使用法

<?php
/**
 * @codeCoverageIgnore
 */
class Foo
{
    public function bar()
    {
    }
}

class Bar
{
    /**
     * @codeCoverageIgnore
     */
    public function foo()
    {
    }
}

if (FALSE) {
    // @codeCoverageIgnoreStart
    print '*';
    // @codeCoverageIgnoreEnd
}

exit; // @codeCoverageIgnore
?>


これらのアノテーションを使って無視するよう指定された行は、 もし実行可能なら (たとえ実行されていなくても) 実行されたものとみなされ、 強調表示されません。

カバーするメソッドの指定

テストコードで @covers アノテーション (表 B.1) を参照ください) を使用すると、 そのテストメソッドがどのメソッドをテストしたいのかを指定することができます。 これを指定すると、指定したメソッドのコードカバレッジ情報のみを考慮します。 例 11.2 に例を示します。

例 11.2: どのメソッドを対象とするかを指定したテスト

<?php
class BankAccountTest extends PHPUnit_Framework_TestCase
{
    protected $ba;

    protected function setUp()
    {
        $this->ba = new BankAccount;
    }

    /**
     * @covers BankAccount::getBalance
     */
    public function testBalanceIsInitiallyZero()
    {
        $this->assertEquals(0, $this->ba->getBalance());
    }

    /**
     * @covers BankAccount::withdrawMoney
     */
    public function testBalanceCannotBecomeNegative()
    {
        try {
            $this->ba->withdrawMoney(1);
        }

        catch (BankAccountException $e) {
            $this->assertEquals(0, $this->ba->getBalance());

            return;
        }

        $this->fail();
    }

    /**
     * @covers BankAccount::depositMoney
     */
    public function testBalanceCannotBecomeNegative2()
    {
        try {
            $this->ba->depositMoney(-1);
        }

        catch (BankAccountException $e) {
            $this->assertEquals(0, $this->ba->getBalance());

            return;
        }

        $this->fail();
    }

    /**
     * @covers BankAccount::getBalance
     * @covers BankAccount::depositMoney
     * @covers BankAccount::withdrawMoney
     */
    public function testDepositWithdrawMoney()
    {
        $this->assertEquals(0, $this->ba->getBalance());
        $this->ba->depositMoney(1);
        $this->assertEquals(1, $this->ba->getBalance());
        $this->ba->withdrawMoney(1);
        $this->assertEquals(0, $this->ba->getBalance());
    }
}
?>


あるテストが、一切メソッドをカバーしてはならないことも指定できます。 そのために使うのが @coversNothing アノテーションです。 (「@coversNothing」 を参照ください)。 これは、インテグレーションテストを書く際に ユニットテストだけのコードカバレッジを生成させたい場合に便利です。

例 11.3: どのメソッドもカバーすべきでないことを指定したテスト

<?php
class GuestbookIntegrationTest extends PHPUnit_Extensions_Database_TestCase
{
    /**
     * @coversNothing
     */
    public function testAddEntry()
    {
        $guestbook = new Guestbook();
        $guestbook->addEntry("suzy", "Hello world!");

        $queryTable = $this->getConnection()->createQueryTable(
            'guestbook', 'SELECT * FROM guestbook'
        );

        $expectedTable = $this->createFlatXmlDataSet("expectedBook.xml")
                              ->getTable("guestbook");

        $this->assertTablesEqual($expectedTable, $queryTable);
    }
}
?>
      


エッジケース

この節では、コードカバレッジ情報がわかりにくくなってしまうような、 エッジケースについて紹介します。

例 11.4:

<?php
// カバレッジは「行単位」であって文単位ではないので、
// 一行にまとめられた行はひとつのカバレッジ状態しか持ちません
if (false) this_function_call_shows_up_as_covered();

// コードカバレッジの内部動作上、これら 2 行は特別です。
// 次の行は「実行されていない」となります
if (false)
    // 次の行は「実行されている」となります
    // 実際のところ、ひとつ上の if 文のカバレッジ情報がここに表示されることになるからです!
    will_also_show_up_as_coveraged();

// これを避けるには、必ず波括弧を使わなければなりません
if (false) {
    this_call_will_never_show_up_as_covered();
}
?>


このページの改善案をGitHubで提案してください!