こんにちは。よっしーです(^^)
今日は、PHPUnitにおけるCode Coverageについてご紹介します。
背景
コンピュータ科学において、コードカバレッジ(Code Coverage)は、特定のテストスイートによってプログラムのソースコードがどの程度テストされているかを示す指標です。高いコードカバレッジを持つプログラムは、より徹底的にテストされており、ソフトウェアのバグが含まれる可能性が低くなります。
この記事では、PHPUnitのコードカバレッジ機能について学びます。この機能は、テストが実行される際にプロダクションコードのどの部分が実行されているかを示します。PHPUnitは、php-code-coverageライブラリを利用しており、さらにPHPのPCOVまたはXdebug拡張機能が提供するコードカバレッジ機能を活用しています。
テストの実行中にコードカバレッジドライバがないという警告が表示された場合は、 PHP CLI バイナリ (php) を使用しており、PCOV あるいは Xdebug がロードされていないことを意味します。
コード・カバレッジ・データの収集にXdebugを使いたい場合は、Xdebugのカバレッジ・モードをアクティブにする必要があります。
詳細は下記のサイトを御覧ください。
PHPUnitは、HTML形式のコードカバレッジレポートや、Clover、Cobertura、Crap4J、PHPUnitなどさまざまな形式のコードカバレッジ情報を持つXMLベースのログファイルを生成することができます。コードカバレッジ情報はテキストとして報告されることもあり(標準出力に表示されます)、さらなる処理のためにPHPコードとしてエクスポートすることもできます。
コードカバレッジ機能を制御するためのコマンドラインオプションについては、「The Command-Line Test Runner」を参照してください。また、コードカバレッジを報告するための関連する設定については、「<coverage> Element」を参照してください。
コード・カバレッジのためのソフトウェア評価指標
コードカバレッジの測定には、以下のさまざまなソフトウェアメトリクスが存在します:
- ラインカバレッジ(Line Coverage): ラインカバレッジは、各実行可能な行が実行されたかどうかを測定します。
- ブランチカバレッジ(Branch Coverage): ブランチカバレッジは、各制御構造のブール式がテストスイートを実行する際に真と偽の両方の評価をしたかどうかを測定します。
- パスカバレッジ(Path Coverage): パスカバレッジは、関数やメソッド内の各可能な実行パスがテストスイートの実行時に実行されたかどうかを測定します。実行パスとは、関数やメソッドの入り口から出口までの一意のブランチのシーケンスを指します。
- 関数とメソッドカバレッジ(Function and Method Coverage): 関数とメソッドカバレッジは、各関数やメソッドが呼び出されたかどうかを測定します。php-code-coverageでは、すべての実行可能な行がカバーされている場合にのみ、関数またはメソッドをカバーされたとみなします。
- クラスとトレイトカバレッジ(Class and Trait Coverage): クラスとトレイトカバレッジは、クラスやトレイトの各メソッドがカバーされているかどうかを測定します。php-code-coverageでは、すべてのメソッドがカバーされている場合にのみ、クラスまたはトレイトをカバーされたとみなします。
- Change Risk Anti-Patterns (CRAP) インデックス: Change Risk Anti-Patterns (CRAP) インデックスは、コードのサイクロマチック複雑性とコードカバレッジに基づいて計算されます。複雑すぎず、適切なテストカバレッジを持つコードはCRAPインデックスが低くなります。CRAPインデックスは、テストを書くことやコードのリファクタリングを行うことで低減させることができます。
PHPUnitが使用するライブラリは、上記にリストされたすべてのコードカバレッジのソフトウェアメトリクスをサポートしています。ブランチカバレッジとパスカバレッジを報告するには、Xdebugを使用してコードカバレッジデータを収集する必要があります。なぜなら、PCOVはラインカバレッジのみをサポートしているためです。
includeファイル
PHPUnitでは、コードカバレッジレポートに含めるソースコードファイルを指定するためにフィルタを設定することが必須です。これは、–coverage-filterコマンドラインオプションを使用するか、設定ファイルを介して行うことができます(<include>要素を参照)。
フィルタの使用方法を構成するために、includeUncoveredFilesという設定が利用できます:
- includeUncoveredFiles=”false” の場合、実行されたコードの少なくとも1行を持つファイルのみがコードカバレッジレポートに含まれます。
- includeUncoveredFiles=”true”(デフォルト値)の場合、ファイルのいずれかの行が実行されていなくても、すべてのファイルがコードカバレッジレポートに含まれます
カバーするコードのパーツを指定する方法
PHPUnit\Framework\Attributes\CoversClassおよびPHPUnit\Framework\Attributes\CoversFunction属性は、テストコード内で使用され、テストクラスがカバーするコードの対象を指定するために使用されます。
これらの属性をテストケースクラスに使用すると、このテストケースクラスのテストメソッドが実行される際に、指定されたコードの対象に対するコードカバレッジ情報のみが収集されます。
下記にサンプルを示します。
<?php declare(strict_types=1);
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
#[CoversClass(Invoice::class)]
#[UsesClass(Money::class)]
final class InvoiceTest extends TestCase
{
public function testAmountInitiallyIsEmpty(): void
{
$this->assertEquals(new Money, (new Invoice)->amount());
}
}
PHPUnit\Framework\Attributes\UsesClassおよびPHPUnit\Framework\Attributes\UsesFunction属性は、コードカバレッジから除外されるべきコードの対象を指定するために使用されますが、カバーされるコードで使用することが許可されています。これは、「誤ってカバーされたコード」のセクションで説明されています。
PHPUnit\Framework\Attributes\CoversNothing属性は、テストがコードカバレッジに寄与しないように指定するために使用されます。これは、統合テストを書く際や、ユニットテストでのみコードカバレッジを生成することを確認する際に役立ちます。
<?php declare(strict_types=1);
use PHPUnit\Framework\Attributes\CoversNothing;
use PHPUnit\Framework\TestCase;
#[CoversNothing]
final class IntegrationTest extends TestCase
{
public function testRegisteredUserCanLogIn(): void
{
// ...
}
}
コードブロックの無視
時には、テストできないコードの単位があることがあり、コードカバレッジの解析時に無視したい場合があります。PHPUnitでは、IgnoreClassForCodeCoverage、IgnoreMethodForCodeCoverage、IgnoreFunctionForCodeCoverage属性を使用してこれを行うことができます。
PHPUnit 10.1以降では、本番コードで使用できるPHPUnit\Framework\Attributes\CodeCoverageIgnore属性および@codeCoverageIgnoreStartおよび@codeCoverageIgnoreEndアノテーションが非推奨となりました。なるべく早く、上記で説明した属性に移行することをお勧めします。
PHPUnit\Framework\Attributes\CodeCoverageIgnore属性は、本番コードのクラスレベルおよびメソッドレベルで使用できます。@codeCoverageIgnoreStartおよび@codeCoverageIgnoreEndアノテーションは、メソッドの本体内部で使用でき、例えば、本番コードの個々の行を無視するために使用されます。
<?php declare(strict_types=1);
use PHPUnit\Framework\Attributes\CodeCoverageIgnore;
use PHPUnit\Framework\TestCase;
#[CodeCoverageIgnore]
final class Foo
{
public function bar(): void
{
}
}
final class Bar
{
#[CodeCoverageIgnore]
public function foo(): void
{
}
}
if (false) {
// @codeCoverageIgnoreStart
print '*';
// @codeCoverageIgnoreEnd
}
exit; // @codeCoverageIgnor
おわりに
PHPUnitにおけるCode Coverageについてご紹介しました。
何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。
それでは、また明日お会いしましょう(^^)
コメント