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

 

The beauty of testing is found not in the effort but in the efficiency.

- テストの美学は、どれだけ汗を流したかではなく どれだけ効率的であるかである。

Knowing what should be tested is beautiful, and knowing what is being tested is beautiful.

- 何をテストすべきなのかを知ること、何がテストされているのかを知ること。 これが大切だ。

 
 --Murali Nandigama

この章では、PHPUnit のコードカバレッジ機能について学びます。 これは、テストを実行したときに、実装コードのどの部分が実行されたかを調べるものです。 次のような疑問に対する答えとなるでしょう。

ステートメントカバレッジというのは、たとえば 100 行のコードで構成されるメソッドがあった場合に、 もしテストで実際に実行されたのがそのうちの 75 行だけだったなら、 そのメソッドのコードカバレッジは 75 パーセントだと考えるということです。

PHPUnit のコードカバレッジ解析では PHP_CodeCoverage コンポーネントを使っています。このコンポーネントは、 Xdebug 拡張モジュールが提供するステートメントカバレッジ機能を利用しています。

注記

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

それでは、BankAccount クラスについての コードカバレッジレポートを作成してみましょう。

phpunit --coverage-html ./report BankAccountTest
PHPUnit 4.2.0 by Sebastian Bergmann.

...

Time: 0 seconds

OK (3 tests, 3 assertions)

Generating report, this may take a moment.

図 11.1 は、コードカバレッジレポートの一部を抜粋したものです。 テスト時に実行された行は、緑色で強調表示されます。 実行可能なコードであるにもかかわらず実行されなかった行については赤色で強調表示されます。 また、"無意味なコード" についてはグレーで強調表示されます。 行の左にある数字は、その行をカバーするテストの数を表します。

図11.1 setBalance() のコードカバレッジ

setBalance() のコードカバレッジ


BankAccount のコードカバレッジレポートからわかることは、 setBalance()depositMoney() をコールするテストがまだ存在しないということ、 そして withdrawMoney() に正しい値を指定した場合のテストも存在しないということです。 BankAccountTest クラスに追加するテストを 例 11.1 に示します。これによって、BankAccount クラスのテストケースを完全に網羅できるようになります。

例 11.1: 完全なコードカバレッジを達成するために欠けているテスト

<?php
require_once 'BankAccount.php';

class BankAccountTest extends PHPUnit_Framework_TestCase
{
    // ...

    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());
    }
}
?>


図 11.2 は、 テストを追加した後の setBalance() のコードカバレッジです。

図11.2 setBalance() にテストを追加した後のコードカバレッジ

setBalance() にテストを追加した後のコードカバレッジ


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

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

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

<?php
require_once 'BankAccount.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);
    }
}
?>
      


コードブロックの無視

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

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

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

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

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


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

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

デフォルトでは、1 行でもコードが実行されたソースコードファイルはすべて (そしてそのようなファイルのみが) レポートに含められます。 レポートに含まれるソースコードファイルは、 ホワイトリスト方式あるいはブラックリスト方式でフィルタリングすることができます。

ブラックリストには、PHPUnit 自身のソースコードファイルやテストファイルがデフォルトで登録されています。 ホワイトリストが空 (デフォルト) の場合はブラックリストを使用し、 ホワイトリストが空でない場合はホワイトリストを使用します。 ホワイトリスト内の各ファイルは、そのファイルが実行されるかどうかにかかわらず レポートに追加されます。追加されたファイルのすべての行は、 実行不能な行も含めて「実行されなかった行」とみなします。

PHPUnit の設定で processUncoveredFilesFromWhitelist="true" (「コードカバレッジ対象のファイルの追加や除外」 を参照ください) とすると、これらのファイルが PHP_CodeCoverage に渡され、実行可能な行数を適切に算出します。

注記

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

PHPUnit の XML 設定ファイル (「コードカバレッジ対象のファイルの追加や除外」 を参照ください) を使って、ブラックリストやホワイトリストを制御することができます。 おすすめの方法は、ホワイトリスト方式を使ってコードカバレッジレポートに含めるファイルを制御することです。

エッジケース

ほとんどの場面では、PHPUnit が「行単位の」コードカバレッジ情報を提供してくれると考えて間違いないでしょう。 しかし、その情報収集の方法が原因で、特筆すべきエッジケースもいくつか存在します。

例 11.5:

<?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();
}
?>