9. 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.

In this chapter you will learn all about PHPUnit’s code coverage functionality that provides an insight into what parts of the production code are executed when the tests are run. It makes use of the php-code-coverage component, which in turn leverages the code coverage functionality provided by the Xdebug or PCOV extensions for PHP or by PHPDBG.

Note

If you see a warning while running tests that no code coverage driver is available, it means that you are using the PHP CLI binary (php) and do not have Xdebug loaded. The Xdebug installation guide explains how Xdebug can be installed and configured. Alternatively, you may use the PHPDBG binary (phpdbg) instead of the PHP CLI one.

PHPUnit can generate an HTML-based code coverage report as well as XML-based logfiles with code coverage information in various formats (Clover, Crap4J, PHPUnit). Code coverage information can also be reported as text (and printed to STDOUT) and exported as PHP code for further processing.

Please refer to The Command-Line Test Runner for a list of command line switches that control code coverage functionality as well as The <logging> Element for the relevant configuration settings.

Software Metrics for Code Coverage

Various software metrics exist to measure code coverage:

Line Coverage

The Line Coverage software metric measures whether each executable line was executed.

Function and Method Coverage

The Function and Method Coverage software metric measures whether each function or method has been invoked. php-code-coverage only considers a function or method as covered when all of its executable lines are covered.

Class and Trait Coverage

The Class and Trait Coverage software metric measures whether each method of a class or trait is covered. php-code-coverage only considers a class or trait as covered when all of its methods are covered.

Opcode Coverage

The Opcode Coverage software metric measures whether each opcode of a function or method has been executed while running the test suite. A line of code usually compiles into more than one opcode. Line Coverage regards a line of code as covered as soon as one of its opcodes is executed.

Branch Coverage

The Branch Coverage software metric measures whether the boolean expression of each control structure evaluated to both true and false while running the test suite.

Path Coverage

The Path Coverage software metric measures whether each of the possible execution paths in a function or method has been followed while running the test suite. An execution path is a unique sequence of branches from the entry of the function or method to its exit.

Change Risk Anti-Patterns (CRAP) Index

The Change Risk Anti-Patterns (CRAP) Index is calculated based on the cyclomatic complexity and code coverage of a unit of code. Code that is not too complex and has an adequate test coverage will have a low CRAP index. The CRAP index can be lowered by writing tests and by refactoring the code to lower its complexity.

Note

The Opcode Coverage, Branch Coverage, and Path Coverage software metrics are not yet supported by php-code-coverage.

Whitelisting Files

It is mandatory to configure a whitelist for telling PHPUnit which sourcecode files to include in the code coverage report. This can either be done using the --whitelist command line option or via the configuration file (see The <filter> Element).

The addUncoveredFilesFromWhitelist and processUncoveredFilesFromWhitelist configuration settings are available to configure how the whitelist is used:

  • addUncoveredFilesFromWhitelist="false" means that only whitelisted files that have at least one line of executed code are included in the code coverage report
  • addUncoveredFilesFromWhitelist="true" (default) means that all whitelisted files are included in the code coverage report even if not a single line of code of such a file is executed
  • processUncoveredFilesFromWhitelist="false" (default) means that a whitelisted file that has no executed lines of code will be added to the code coverage report (if addUncoveredFilesFromWhitelist="true" is set) but it will not be loaded by PHPUnit and it will therefore not be analysed for correct executable lines of code information
  • processUncoveredFilesFromWhitelist="true" means that a whitelisted file that has no executed lines of code will be loaded by PHPUnit so that it can be analysed for correct executable lines of code information

Note

Please note that the loading of sourcecode files that is performed when processUncoveredFilesFromWhitelist="true" is set can cause problems when a sourcecode file contains code outside the scope of a class or function, for instance.

Ignoring Code Blocks

Sometimes you have blocks of code that you cannot test and that you may want to ignore during code coverage analysis. PHPUnit lets you do this using the @codeCoverageIgnore, @codeCoverageIgnoreStart and @codeCoverageIgnoreEnd annotations as shown in Example 9.1.

Example 9.1 Using the @codeCoverageIgnore, @codeCoverageIgnoreStart and @codeCoverageIgnoreEnd annotations

  1. <?php declare(strict_types=1);
  2. use PHPUnit\Framework\TestCase;
  3. /**
  4. * @codeCoverageIgnore
  5. */
  6. final class Foo
  7. {
  8. public function bar(): void
  9. {
  10. }
  11. }
  12. final class Bar
  13. {
  14. /**
  15. * @codeCoverageIgnore
  16. */
  17. public function foo(): void
  18. {
  19. }
  20. }
  21. if (false) {
  22. // @codeCoverageIgnoreStart
  23. print '*';
  24. // @codeCoverageIgnoreEnd
  25. }
  26. exit; // @codeCoverageIgnore

The ignored lines of code (marked as ignored using the annotations) are counted as executed (if they are executable) and will not be highlighted.

Specifying Covered Code Parts

The @covers annotation (see the annotation documentation) can be used in the test code to specify which code parts a test class (or test method) wants to test. If provided, this effectively filters the code coverage report to include executed code from the referenced code parts only. Example 9.2 shows an example.

Note

If a method is specificed with the @covers annotation, only the referenced method will be considered as covered, but not methods called by this method. Hence, when a covered method is refactored using the extract method refactoring, corresponding @covers annotations need to be added. This is the reason it is recommended to use this annotation with class scope, not with method scope.

Example 9.2 Test class that specifies which class it wants to cover

  1. <?php declare(strict_types=1);
  2. use PHPUnit\Framework\TestCase;
  3. /**
  4. * @covers \Invoice
  5. * @uses \Money
  6. */
  7. final class InvoiceTest extends TestCase
  8. {
  9. private $invoice;
  10. protected function setUp(): void
  11. {
  12. $this->invoice = new Invoice;
  13. }
  14. public function testAmountInitiallyIsEmpty(): void
  15. {
  16. $this->assertEquals(new Money, $this->invoice->getAmount());
  17. }
  18. }

Example 9.3 Tests that specify which method they want to cover

  1. <?php declare(strict_types=1);
  2. use PHPUnit\Framework\TestCase;
  3. final class BankAccountTest extends TestCase
  4. {
  5. private $ba;
  6. protected function setUp(): void
  7. {
  8. $this->ba = new BankAccount;
  9. }
  10. /**
  11. * @covers \BankAccount::getBalance
  12. */
  13. public function testBalanceIsInitiallyZero(): void
  14. {
  15. $this->assertSame(0, $this->ba->getBalance());
  16. }
  17. /**
  18. * @covers \BankAccount::withdrawMoney
  19. */
  20. public function testBalanceCannotBecomeNegative(): void
  21. {
  22. try {
  23. $this->ba->withdrawMoney(1);
  24. }
  25. catch (BankAccountException $e) {
  26. $this->assertSame(0, $this->ba->getBalance());
  27. return;
  28. }
  29. $this->fail();
  30. }
  31. /**
  32. * @covers \BankAccount::depositMoney
  33. */
  34. public function testBalanceCannotBecomeNegative2(): void
  35. {
  36. try {
  37. $this->ba->depositMoney(-1);
  38. }
  39. catch (BankAccountException $e) {
  40. $this->assertSame(0, $this->ba->getBalance());
  41. return;
  42. }
  43. $this->fail();
  44. }
  45. /**
  46. * @covers \BankAccount::getBalance
  47. * @covers \BankAccount::depositMoney
  48. * @covers \BankAccount::withdrawMoney
  49. */
  50. public function testDepositWithdrawMoney(): void
  51. {
  52. $this->assertSame(0, $this->ba->getBalance());
  53. $this->ba->depositMoney(1);
  54. $this->assertSame(1, $this->ba->getBalance());
  55. $this->ba->withdrawMoney(1);
  56. $this->assertSame(0, $this->ba->getBalance());
  57. }
  58. }

It is also possible to specify that a test should not cover any method by using the @coversNothing annotation (see @coversNothing). This can be helpful when writing integration tests to make sure you only generate code coverage with unit tests.

Example 9.4 A test that specifies that no method should be covered

  1. <?php declare(strict_types=1);
  2. use PHPUnit\DbUnit\TestCase
  3. final class GuestbookIntegrationTest extends TestCase
  4. {
  5. /**
  6. * @coversNothing
  7. */
  8. public function testAddEntry(): void
  9. {
  10. $guestbook = new Guestbook();
  11. $guestbook->addEntry("suzy", "Hello world!");
  12. $queryTable = $this->getConnection()->createQueryTable(
  13. 'guestbook', 'SELECT * FROM guestbook'
  14. );
  15. $expectedTable = $this->createFlatXmlDataSet("expectedBook.xml")
  16. ->getTable("guestbook");
  17. $this->assertTablesEqual($expectedTable, $queryTable);
  18. }
  19. }

Edge Cases

This section shows noteworthy edge cases that lead to confusing code coverage information.

  1. <?php declare(strict_types=1);
  2. use PHPUnit\Framework\TestCase;
  3. // Because it is "line based" and not statement base coverage
  4. // one line will always have one coverage status
  5. if (false) this_function_call_shows_up_as_covered();
  6. // Due to how code coverage works internally these two lines are special.
  7. // This line will show up as non executable
  8. if (false)
  9. // This line will show up as covered because it is actually the
  10. // coverage of the if statement in the line above that gets shown here!
  11. will_also_show_up_as_covered();
  12. // To avoid this it is necessary that braces are used
  13. if (false) {
  14. this_call_will_never_show_up_as_covered();
  15. }

Speeding Up Code Coverage with Xdebug

The performance of code coverage data collection with Xdebug 2.6 (and later) can be significantly improved by delegating whitelist filtering to Xdebug.

In order to do this, the first step is to generate the filter script for Xdebug using the --dump-xdebug-filter option:

  1. $ phpunit --dump-xdebug-filter build/xdebug-filter.php
  2. PHPUnit 7.4.0 by Sebastian Bergmann and contributors.
  3. Runtime: PHP 7.2.11 with Xdebug 2.6.1
  4. Configuration: /workspace/project/phpunit.xml
  5. Wrote Xdebug filter script to build/xdebug-filter.php

Now we can use the --prepend option to load the Xdebug filter script as early as possible when we want to generate a code coverage report:

  1. $ phpunit --prepend build/xdebug-filter.php --coverage-html build/coverage-report