.. _code-coverage-analysis: ==================== コードカバレッジ解析 ==================== *Wikipedia*: 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. この章では、PHPUnit のコードカバレッジ機能について学びます。 これは、テストを実行したときに、実装コードのどの部分が実行されたかを調べるものです。 PHPUnit のコードカバレッジ解析では `php-code-coverage `_ コンポーネントを使っています。このコンポーネントは、 `Xdebug `_ や `PCOV `_ などのPHP拡張モジュールが提供するコードカバレッジ機能、もしくは `PHPDBG `_ などが提供するコードカバレッジ機能を利用しています。 .. admonition:: Note テストの実行中に「 no code coverage drive is available 」という WARNING のエラーが表示された場合、 それは利用している PHP の CLI バイナリ (``php``) に Xdebug や PCOV が読み込まれていないということを示します。 PHPUnit は、HTML ベースのコードカバレッジレポートを生成するだけでなく、 XML ベースのログファイルにコードカバレッジ情報を出力することもできます。 Clover、Crap4J、PHPUnit など、さまざまな形式に対応しています。 また、コードカバレッジ情報をテキスト形式で出力 (そして、標準出力に表示) したり、PHP のコードとして出力して後処理をしたりすることもできます。 コードカバレッジ機能を制御するための コマンドラインスイッチの一覧は、:ref:`textui` を参照ください。 また、設定項目については :ref:`appendixes.configuration.logging` を参照ください。 .. _code-coverage-analysis.metrics: コードカバレッジの指標 ###################### コードカバレッジを計測するための指標には、さまざまなものがあります。 *Line Coverage* *ラインカバレッジ* は、 実行可能な行が実行されたかどうかを計測します。 *Branch Coverage* *ブランチカバレッジ* は、テストスイートの実行中に、 制御構造内の boolean 式が ``true`` あるいは ``false`` のどちらかとして評価されたかどうかを計測します。 *Path Coverage* *パスカバレッジ* は、テストスイートの実行中に、 関数やメソッド内で取りうる実行パスが網羅されたかどうかを計測します。 実行パスとは、関数やメソッドに入ってから出るまでの間のルート内での分岐のことです。 *Function and Method Coverage* *関数・メソッドカバレッジ* は、 関数やメソッドが実行されたかどうかを計測します。 php-code-coverage は、その関数やメソッド内の実行可能な行がすべて実行された場合にのみ、 その関数やメソッドが実行されたとみなします。 *クラス・トレイトカバレッジ* *クラス・トレイトカバレッジ* は、 クラスやトレイトがカバーされたかどうかを計測します。 php-code-coverage は、クラスやトレイト内のすべてのメソッドがカバーされている場合にのみ、 そのクラスやトレイトがカバーされたとみなします。 *Change Risk Anti-Patterns (CRAP) Index* *Change Risk Anti-Patterns (CRAP) インデックス* とは、循環的複雑度と、あるコード単位のコードカバレッジに基づいて算出される指標です。 複雑度が低く、適切なテストカバレッジが達成されているコードは、CRAPインデックスの値が低くなります。 CRAPインデックスを下げるには、テストを書くか、 あるいはリファクタリングでコードの複雑性を下げます。 .. _code-coverage-analysis.including-files: コードカバレッジレポートに含めるファイルの指定 ################################################ フィルターを設定して、 PHPUnit に対してどのソースコードファイルをコードカバレッジレポートに含めるかを指定する必要があります。 フィルターの設定には、コマンドラインオプションの ``--coverage-filter`` :ref:`command line ` を使うか、あるいは設定ファイルを使います ( :ref:`appendixes.configuration.whitelisting-files` を参照ください)。 設定項目 ``includeUncoveredFilesInCodeCoverageReport`` および ``processUncoveredFilesForCodeCoverageReport`` で、フィルターの使いかたを設定できます。 - ``includeUncoveredFilesInCodeCoverageReport="false"`` と指定すると、指定したファイルのうち、実行される行が少なくとも一行以上あるファイルだけをコードカバレッジレポートに含めます。 - ``includeUncoveredFilesInCodeCoverageReport="true"`` (デフォルト) と指定すると、実行されるコードが一行もないファイルを含めて、フィルターで指定したすべてのファイルをコードカバレッジレポートに含めます。 - ``processUncoveredFilesForCodeCoverageReport="false"`` (デフォルト) と指定すると、フィルターで指定したファイルのうち、実行される行が一行もないファイルも (``includeUncoveredFilesInCodeCoverageReport="true"`` が指定されている場合) コードカバレッジレポートに追加されますが、PHPUnit には読み込まれず、実行可能な行数の解析は行われません。 - ``processUncoveredFilesForCodeCoverageReport="true"`` と指定すると、フィルターで指定したファイルのうち、実行される行が一行もないファイルも PHPUnit に読み込まれて、実行可能な行数の解析も行われるようになります。 .. admonition:: Note ``processUncoveredFilesForCodeCoverageReport="true"`` が設定されている場合のソースコードファイルの読み込みでは、 もしクラスや関数のスコープから外れるコードが含まれていたときに問題が起こる可能性があります。 .. _code-coverage-analysis.ignoring-code-blocks: コードブロックの無視 #################### どうしてもテストができないコードブロックなどを、 コードカバレッジ解析時に無視させたいこともあるでしょう。 PHPUnit でこれを実現するには、 ``@codeCoverageIgnore``、 ``@codeCoverageIgnoreStart`` および ``@codeCoverageIgnoreEnd`` アノテーションを :numref:`code-coverage-analysis.ignoring-code-blocks.examples.Sample.php` のように使用します。 .. code-block:: php :caption: ``@codeCoverageIgnore``、``@codeCoverageIgnoreStart`` および ``@codeCoverageIgnoreEnd`` アノテーションの使用法 :name: code-coverage-analysis.ignoring-code-blocks.examples.Sample.php ` を参照ください) を使用すると、 そのテストクラスやテストメソッドがどのコードパーツをテストしたいのかを指定することができます。 これを指定すると、指定したコードパーツのコードカバレッジ情報のみを考慮します。 :numref:`code-coverage-analysis.specifying-covered-parts.examples.InvoiceTest.php` に例を示します。 .. admonition:: Note ``@covers`` アノテーションでメソッドを指定した場合、そのメソッドがテストによってカバーされているどうかのみが判定されます。このメソッドによって呼び出された他のメソッドがテストによってカバーされたかどうかは判定されません。 そのため、 ``@covers`` アノテーションをつけたメソッドに対して *Extract Method* (メソッドの抽出) のリファクタリングを行う場合は、新しく抽出したメソッドに適宜 ``@covers`` アノテーションを追加する必要があります。 これが、このアノテーションをクラス単位で利用することを推奨している理由です。 .. code-block:: php :caption: どのクラスを対象とするかを指定したテスト :name: code-coverage-analysis.specifying-covered-parts.examples.InvoiceTest.php invoice = new Invoice; } public function testAmountInitiallyIsEmpty(): void { $this->assertEquals(new Money, $this->invoice->getAmount()); } } .. code-block:: php :caption: どのメソッドを対象とするかを指定したテスト :name: code-coverage-analysis.specifying-covered-parts.examples.BankAccountTest.php ba = new BankAccount; } /** * @covers \BankAccount::getBalance */ public function testBalanceIsInitiallyZero(): void { $this->assertSame(0, $this->ba->getBalance()); } /** * @covers \BankAccount::withdrawMoney */ public function testBalanceCannotBecomeNegative(): void { try { $this->ba->withdrawMoney(1); } catch (BankAccountException $e) { $this->assertSame(0, $this->ba->getBalance()); return; } $this->fail(); } /** * @covers \BankAccount::depositMoney */ public function testBalanceCannotBecomeNegative2(): void { try { $this->ba->depositMoney(-1); } catch (BankAccountException $e) { $this->assertSame(0, $this->ba->getBalance()); return; } $this->fail(); } /** * @covers \BankAccount::getBalance * @covers \BankAccount::depositMoney * @covers \BankAccount::withdrawMoney */ public function testDepositWithdrawMoney(): void { $this->assertSame(0, $this->ba->getBalance()); $this->ba->depositMoney(1); $this->assertSame(1, $this->ba->getBalance()); $this->ba->withdrawMoney(1); $this->assertSame(0, $this->ba->getBalance()); } } あるテストが、*一切* メソッドをカバーしてはならないことも指定できます。 そのために使うのが ``@coversNothing`` アノテーションです。 (:ref:`appendixes.annotations.coversNothing` を参照ください)。 これは、インテグレーションテストを書く際に ユニットテストだけのコードカバレッジを生成させたい場合に便利です。 .. code-block:: php :caption: どのメソッドもカバーすべきでないことを指定したテスト :name: code-coverage-analysis.specifying-covered-parts.examples.GuestbookIntegrationTest.php addEntry("suzy", "Hello world!"); $queryTable = $this->getConnection()->createQueryTable( 'guestbook', 'SELECT * FROM guestbook' ); $expectedTable = $this->createFlatXmlDataSet("expectedBook.xml") ->getTable("guestbook"); $this->assertTablesEqual($expectedTable, $queryTable); } } .. _code-coverage-analysis.edge-cases: エッジケース ############ この節では、コードカバレッジ情報がわかりにくくなってしまうような、 エッジケースについて紹介します。 .. code-block:: php :name: code-coverage-analysis.edge-cases.examples.Sample.php