关于代码覆盖率

现在我们已经探讨了不同的测试类型,你可能想马上开始编写测试。不过,在你收起这本书开始编码之前,让我们以 "你应该测试多少代码" 这个问题来结束本章。

我们在第 8 章 "代码质量度量" 中谈到代码质量度量时,已经简要地提到了代码覆盖率的概念。现在,让我们仔细研究一下这个概念。

了解代码覆盖率

代码覆盖率 衡量的是测试覆盖代码的比例。代码覆盖率越高越好—​如果测试越多,软件包含错误的可能性就越小,也就越难引入新的错误而不被发现。更高的代码覆盖率也可能是代码质量更好的一个指标—​正如我们在本章前一节所讨论的,经过测试的代码必须以某种方式编写,这通常会带来更好的质量。

一般来说,覆盖率的高低可以简单地用已测试代码的百分比来表示,即从 0%(完全未测试)到 100% (完全代码覆盖)。但如何衡量代码覆盖率呢?为此,我们将使用 PHPUnit,因为它可以为我们创建代码覆盖率报告。不过,它需要一个额外的 PHP 扩展来实现代码覆盖率功能。在本章中,我们决定使用标准的 PHP 调试器剖析器 Xdebug

设置 Xdebug

Xdebug 是 PHP 的扩展,因此需要作为模块加载。由于其安装比较复杂,主要取决于运行 PHP 的操作系统,因此请参考 https://xdebug.org 上的官方文档,了解如何安装和配置。互联网上也有很多相关教程。

如果对代码进行了重构,你可能想知道这些更改对性能有什么影响。是缩短了执行时间,还是变得更糟了?使用所谓的 剖析器,你可以详细测量每个函数的执行时间,并了解瓶颈隐藏在哪里。

我们无法在书中介绍这一主题,但由于在本章中我们已经使用了 Xdebug,你可能也想了解一下它的剖析功能: https://xdebug.org/docs/profiler 。其它可提供更多便利的商业服务有—​例如--Tideways (https://tideways.com) 或 Blackfire (https://www.blackfire.io)。

Xdebug 替代品

请注意,你可以使用其它扩展来实现这一点,例如 PCOV ( https://github.com/krakjoe/pcov ),如果你只想做代码覆盖率报告,它的性能会更好。不过,Xdebug 是一个非常有用的调试器,你应该了解它—​如果你不了解,我们建议你查看一些相关教程。

如何生成代码覆盖率报告

为了演示如何创建代码覆盖率报告,我们将使用本章上一节中的小演示程序。要跟进示例,请从 GitHub 上查看,运行 composer install,并确保已安装 Xdebug,且 mode 设置为 converage

在开始生成报告之前,让我们看看 PHPUnit 提供了哪些报告格式。它可以生成各种格式的报告,而这些格式很可能是你现在不需要的,如 CloverCoberturaCrap4JPHPUnit XML 格式。不过,当你开始将 PHPUnit 与其它工具集成时,它们就会变得更加重要。

不过,我们不想在本书中讨论这个问题,因此我们只对两种最容易使用的格式感兴趣:文本和 HTML。文本格式可以直接在命令行中打印,这在需要即时结果或将 PHPUnit 集成到构建管道中时非常有用,而 HTML 格式则提供了更多信息。

在我们的示例中,我们希望将两种报告格式都写入项目根目录下名为 reports 的新文件夹中。虽然可以使用许多 PHPUnit 运行时选项生成报告,但我们还是希望使用 phpunit.xml 配置文件来定义每次测试运行时生成的报告。下面的代码片段显示的是一个最小版本,为便于阅读而进行了缩减。在我们的 GitHub 仓库中,你可以找到完整的 phpunit.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php">
    <testsuites>
        <testsuite name="default">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
    <coverage>
        <include>
            <directory suffix=".php">src</directory>
        </include>
        <report>
            <html outputDirectory="reports/coverage" />
            <text outputFile="reports/coverage.txt" />
        </report>
    </coverage>
</phpunit>

除了基本配置(其中包括定期测试运行所需的 tests 文件夹定义)外,我们还添加了 <coverage> 节点。它包含两个子节点: <includes><report>。使用 <includes> 节点时,指定哪个目录和文件扩展名用于收集代码覆盖率信息非常重要。否则,PHPUnit 将不会生成任何报告,也不会抱怨信息缺失。这有时会让人相当困惑。

此外,我们还需要告诉 PHPUnit 在哪里写报告。为此,我们使用 <report> 节点,如你所见,我们指定将 HTML 和文本报告写入项目根目录下的 reports 文件夹。

PHPUnit 希望配置文件名为 phpunit.xml,并位于项目根目录下。如果配置文件已创建,则可通过运行以下命令快速生成报告,无需任何其它选项或参数:

$ vendor/bin/phpunit

执行上述命令后,你会发现在项目根目录下生成了一个 reports 文件夹。它应该包含两样东西:第一,包含文本格式报告的 coverage.txt 文件;第二,包含 HTML 报告的 coverage 文件夹。

代码覆盖代价高昂

使用 Xdebug 生成代码覆盖率报告会减慢测试套件的执行速度,因为 Xdebug 需要收集大量数据,而且它不是为性能而设计的。因此,我们建议你仅在必要时才启用 Xdebug 和报告生成功能,在常规测试运行期间将其禁用。

文本报告很短,但已告诉你测试对应用程序的覆盖程度,如下面的截图所示:

image 2023 11 12 15 45 30 446
Figure 1. Figure 10.2: Text code coverage report

要了解更多详情,请在浏览器中打开 reports/coverage/index.html 文件。它应该是这样的:

image 2023 11 12 15 46 24 793
Figure 2. Figure 10.3: HTML code coverage report

你可以在这里找到与文本报告相同的信息,但更加直观。此外,报告还是交互式的。例如,如果点击左侧的 MyOtherClass.php 链接,就会跳转到该类的详细报告,如下图所示:

image 2023 11 12 15 47 14 328
Figure 3. Figure 10.4: HTML code coverage report – class view

这里有两件事值得注意:首先,在 "函数和方法" 部分,你可能已经认识到了我们在第 8 章 "代码质量度量" 中介绍的 CRAP 度量。在这里,你终于可以看到它的实际应用。

其次,报告会详细显示哪些行在测试过程中被访问过(绿色背景),哪些没有被访问(红色背景)。如果有任何行完全无法访问(例如,在最后一条返回语句之后的另一条语句),则会显示为 死代码(黄色背景)。死代码可以安全地删除。

现在,你对项目的代码覆盖范围有了一个很好的概览。如果文件显示为红色条,则说明在测试运行过程中根本没有执行过这些文件,因此你可以在此改进你的测试套件。

使用 @covers 注释

代码覆盖率有一个问题:它能告诉你哪些代码在测试过程中被执行过,但这并不意味着被执行的代码也经过了测试(即使用断言)。这是 PHPUnit 无法自动判断的。这意味着,即使代码覆盖率报告显示 100% 且到处都是绿条,也并不意味着代码经过了良好测试。它只是在测试套件运行期间被执行了。

为了克服这个问题,建议在类级别使用 @covers 注解,如图所示:

/**
 * @covers MyRepository
 */
class MyRepositoryTest extends TestCase
{
    public function testGetDataReturnsAnArray(): void
    {
        // ...
    }
}

这可以提高测试的准确性,因为通过使用 @covers 注解,我们可以明确声明哪些代码是测试要测试的。例如,假设被测类使用了一个外部服务。你只想测试这一个类,而不是它所使用的服务,因此你只需编写检查被测类的断言。如果没有 @covers 注解,PHPUnit 仍会将外部服务包含在代码覆盖率报告中,因为它在测试过程中被执行了。

你也可以在方法级别使用 @covers;但是,如果你重构了一个类,并将方法提取到其它类中,这可能会造成问题。如果忘记在方法层调整 @covers 注解,覆盖率报告将不再准确。

要强制使用 @covers 注解,请使用 phpunit.xml 文件中的 forceCoversAnnotation 选项。如果将该选项设置为 true,未使用注释的测试将被标记为风险测试;这些测试不会失败,但会在报告中作为有待改进的内容单独出现。这样,你的开发人员(和你自己)就不会忘记使用它了。

要测试什么

我们现在知道了如何获取关于有多少代码经过测试的详细信息。那么,现在是否应该努力实现完整的代码覆盖率?100% 应该是你的目标吗?

正如我们在本章前一部分的示例应用程序测试中所看到的,为一个类编写测试并不自动意味着你真的测试了它的方方面面。在这里,不幸的是,即使测量代码覆盖率也无济于事。不过,它可以帮助你识别那些什么都没测试的测试。尤其是当测试用例中使用了大量模拟时,可能会出现只测试模拟而不测试 "真实代码" 的情况。请看下面的测试用例,这是一个可以通过的有效测试:

public function testUselessTestCase(): void
{
    $repositoryMock = $this->createMock(MyRepository::class);
    $repositoryMock
        ->method('getData')
        ->willReturn([
            'value_1' => 'a',
            'value_2' => 'b'
        ]);
    $this->assertEquals(
        [
            'value_1' => 'a',
            'value_2' => 'b'
        ],
        $repositoryMock->getData()
    );
}

这个例子是简化过的,但它展示了代码覆盖率报告可以提供的帮助,因为这个测试不会给我们的代码覆盖率增加任何测试行。遗憾的是,目前还没有任何工具能告诉你哪些测试写得好,哪些应该改进,甚至像我们的例子一样毫无用处。

根据帕累托原则,以 80% 的代码覆盖率为目标应该已经大大改善了你的代码库,而且只要付出合理的努力就能实现。将重点放在使应用程序与众不同的代码上—​通常称为业务逻辑。这是最需要你关注的代码。

Pareto原则

Pareto原则指出,用 20% 的努力可以取得 80% 的成果。剩下的 20% 的成果需要用 80% 的努力来完成数量上最多的工作。

还有一些琐碎的代码并不需要测试。一个常见的例子就是测试 getter 和 setter。如果这些方法包含进一步的逻辑,测试它们当然是有意义的。但如果它们只是设置或返回属性值的简单函数,那么为它们编写测试代码就是浪费时间。不过,如果你想实现 100% 的代码覆盖率,还是需要这样做。

其它例子包括配置文件、工厂或路由定义。使用 E2E 或集成测试就足够了,它们能确保应用程序在总体上正常运行。它们隐含地(即不使用具体断言)测试了所有胶合代码,这些代码能将应用程序连接在一起,但测试起来比较繁琐。

特别是,E2E 测试通常不计入代码覆盖率指标,因为这样做在技术上很困难。不过,如果你有这些测试,它们会增加一层无法衡量的测试覆盖率。你不能炫耀 100% 的代码覆盖率,但你知道所有不同类型的测试都在支持你,这应该是我们的首要目标。

总结

在本章中,我们讨论了为什么要使用自动化测试,以及自动化测试如何提高代码质量。我们介绍了主要的三种测试类型,即单元测试、集成测试和 E2E 测试,以及它们的优缺点、潜在隐患和我们对如何使用它们的建议。最后,你了解了代码覆盖率的概念,以及如何在自己的项目中使用它。

结合上一章中关于代码质量工具以及如何组织这些工具的知识,在下一章中,我们终于可以开始将所有这些工具结合在一起,形成一个有助于以结构化和可靠的方式运行所有这些工具的流程—​构建管道。

进一步阅读

本章无法涵盖的测试类型还有很多。如果你和作者一样觉得自动化测试的世界很迷人,那么你可能也想了解一下其它测试类型,比如下面这些:

  • 突变测试是指对待测代码进行微小的修改(即所谓的突变)。如果你的测试能捕捉到这些突变,那么它们通常写得不错;否则,它们就会让突变逃脱。Infection 是目前 PHP 界最著名的测试工具 ( https://infection.github.io )。

  • 可视化回归测试是将测试过程中的应用程序截图与原始截图进行比较,以捕捉层叠样式表(CSS)中的问题。虽然这与 PHP 没有直接关系,但如果你想让自己的网络项目保持完美的样式,这对你来说可能很有趣。BackstopJS ( https://github.com/garris/BackstopJS )就是一个不错的选择。

  • API 测试可视为 E2E 测试,但只是针对应用程序可能提供的 API。由于测试是基于超文本传输协议(HTTP)请求进行的,因此不需要无头浏览器,这使得设置更加容易。要开始 API 测试,Codeception (https://codeception.com) 是一个不错的选择。

  • 行为驱动开发(BDD)是一种非常有趣的方法,因为它侧重于利益相关者(例如项目经理)、质量保证(如果有的话)和开发人员之间的沟通。这是通过一种名为 Gherkin 的语言来编写测试的特殊方法来实现的,这种语言基本上可以让非技术人员编写测试套件。PHP 的 BDD 工具称为 Behat ( https://github.com/Behat/Behat )。