Code Coverage Analysis

代码覆盖率分析

在计算机科学中,代码覆盖率是用来描述程序的源代码被特定测试套件测试的程度的度量。与代码覆盖率较低的程序相比,代码覆盖率较高的程序已经过更彻底的测试,并且发现软件缺陷的可能性较低。
--Wikipedia

在本章中,您将学习所有关于PHPUnit的代码覆盖功能,这些功能可以深入了解测试运行时执行生产代码的哪些部分。它利用了PHP_CodeCoverage组件,该组件利用PHP 的Xdebug扩展提供的代码覆盖功能。

Xdebug不作为PHPUnit的一部分进行分发。如果您在运行测试时收到通知,说明Xdebug扩展未加载,则表示Xdebug未安装或未正确配置。在使用PHPUnit中的代码覆盖率分析功能之前,您应该阅读Xdebug安装指南

PHPUnit可以生成基于HTML的代码覆盖率报告以及以各种格式(Clover,Crap4J,PHPUnit)和代码覆盖率信息的基于XML的日志文件。代码覆盖率信息也可以作为文本报告(并打印到STDOUT)并作为PHP代码导出以供进一步处理。

有关控制代码覆盖功能的命令行开关列表以及相关配置设置的“日志记录”部分,请参阅第3章。

代码覆盖率的软件度量

存在各种软件度量来衡量代码覆盖率:

线路覆盖

线Coverage软件度量测量每个可执行行是否被执行。

功能和方法覆盖

函数和方法的覆盖软件度量测量每个函数或方法是否已被调用。PHP_CodeCoverage只考虑覆盖所有可执行行时覆盖的函数或方法。

类别和特质覆盖率

类和特质覆盖软件度量措施一类或性状的每个方法是否被覆盖。PHP_CodeCoverage只考虑覆盖所有方法时所涵盖的类或特征。

操作码覆盖

操作码覆盖软件度量措施是否在运行测试套件的函数或方法的每个操作码已经执行。一行代码通常编译成多个操作码。线路覆盖只要其中一个操作码被执行就会覆盖一行代码。

分支覆盖

分支覆盖软件度量测量每个控制结构的布尔表达式是否评估为两个truefalse在运行测试套件。

路径覆盖

路径覆盖软件度量措施是否在运行测试套件中的每个函数或方法的可能的执行路径的已被遵循。执行路径是从函数或方法输入到其退出的唯一分支序列。

改变风险反模式(CRAP)指数

根据代码单元的圈复杂度和代码覆盖率计算变化风险反模式(CRAP)指数。不太复杂且具有足够测试覆盖率的代码将具有较低的CRAP指数。可以通过编写测试和重构代码来降低CRAP索引的复杂性。

操作码覆盖分支覆盖路径覆盖软件度量尚未被PHP_CodeCoverage支持。

白名单文件

必须配置白名单来告诉PHPUnit哪些源代码文件包含在代码覆盖率报告中。这可以使用--whitelist命令行选项或通过配置文件完成(请参阅“为代码覆盖白名单文件”一节)。

或者,可以通过addUncoveredFilesFromWhitelist="true"在PHPUnit配置中进行设置(请参阅“为代码覆盖白名单文件”一节),将所有列入白名单的文件添加到代码覆盖率报告中。这允许包含尚未测试的文件。例如,如果您想获得有关此类未发现文件的哪些行可执行的信息,则还需要processUncoveredFilesFromWhitelist="true"在PHPUnit配置中进行设置(请参阅“将代码覆盖范围的白名单文件”)一节。

请注意,例如,processUncoveredFilesFromWhitelist="true"当源代码文件包含的代码超出类或函数的范围时,加载设置时执行的源代码文件可能会导致问题。

忽略代码块

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 11.1](code-coverage-analysis#code-coverage-analysis.ignoring-code-blocks.examples.Sample.php).

例11.1:使用 @codeCoverageIgnore**,** 和注释@codeCoverageIgnoreStart @codeCoverageIgnoreEnd

<?php use PHPUnit\Framework\TestCase; /** * @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 use PHPUnit\Framework\TestCase; class BankAccountTest extends 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 use PHPUnit\Framework\TestCase; 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 use PHPUnit\Framework\TestCase; // Because it is "line based" and not statement base coverage // one line will always have one coverage status if (false) this_function_call_shows_up_as_covered( // Due to how code coverage works internally these two lines are special. // This line will show up as non executable if (false) // This line will show up as covered because it is actually the // coverage of the if statement in the line above that gets shown here! will_also_show_up_as_covered( // To avoid this it is necessary that braces are used if (false) { this_call_will_never_show_up_as_covered( } ?>