集成 PHPUnit

编写测试是一项可以自己完成的任务;您只需编写代码,在不满足条件时抛出异常,然后随时运行脚本即可。幸运的是,其他开发人员并不满足于这种手动过程,因此他们开发了一些工具来帮助我们自动完成这一过程并获得良好的反馈。PHP 中使用最多的是 PHPUnit。PHPUnit 是一个框架,它提供了一系列工具,让我们能更轻松地编写测试,自动运行测试,并向开发人员提供有用的反馈。

传统上,为了使用 PHPUnit,我们要在笔记本电脑上安装它。在安装过程中,我们添加了框架的类,包括 PHP 的路径和运行测试的可执行文件。这样做并不理想,因为我们迫使开发人员在他们的开发机器上多安装一个工具。如今,Composer(请参阅第 6 章 "适应 MVC",以加深记忆)可以帮助我们将 PHPUnit 作为项目的依赖项。这意味着运行 Composer(为了获得其余的依赖项,你肯定会这样做)也会获得 PHPUnit。在 composer.json 中添加以下内容:

{
    //...
    "require": {
        "monolog/monolog": "^1.17",
        "twig/twig": "^1.23"
    },
    "require-dev": {
      "phpunit/phpunit": "5.1.3"
    },
    "autoload": {
        "psr-4": {
            "Bookstore\\": "src"
        }
    }
}

请注意,此依赖关系是作为 require-dev 添加的。这意味着只有当我们在开发环境中时才会下载该依赖项,但它不会成为我们在生产环境中部署的应用程序的一部分,因为我们不需要在生产环境中运行测试。要获取依赖项,请一如既往地运行 composer update

另一种不同的方法是全局安装 PHPUnit,这样开发环境中的所有项目都能使用它,而不是每次都在本地安装。有关如何使用 Composer 全局安装工具的信息,请访问 https://akrabat.com/global-installation-of-php-tools-with-composer/

phpunit.xml 文件

PHPUnit 需要一个 phpunit.xml 文件来定义运行测试的方式。该文件定义了一系列规则,如测试位置、测试哪些代码等。在根目录中添加以下文件:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
        backupStaticAttributes="false"
        colors="true"
        convertErrorsToExceptions="true"
        convertNoticesToExceptions="true"
        convertWarningsToExceptions="true"
        processIsolation="false"
        stopOnFailure="false"
        syntaxCheck="false"
        bootstrap="vendor/autoload.php"
>
<testsuites>
    <testsuite name="Bookstore Test Suite">
        <directory>./tests/</directory>
    </testsuite>
</testsuites>
<filter>
    <whitelist>
        <directory>./src</directory>
    </whitelist>
</filter>
</phpunit>

该文件定义了许多内容。最重要的说明如下:

  • 如果将 convertErrorsToExceptionsconvertNoticesToExceptionsconvertWarningsToExceptions 设置为 true,就会在出现 PHP 错误、警告或通知时导致测试失败。这样做的目的是确保代码不包含边缘情况下的小错误,而边缘情况总是潜在问题的根源。

  • stopOnFailure 告诉 PHPUnit,当测试失败时,是否应继续执行其他测试。在本例中,我们希望运行所有测试,以了解有多少测试失败及其原因。

  • Bootstrap 定义了在开始运行测试前应执行的文件。最常见的用法是包含自动加载器,但也可以包含一个初始化某些依赖项(如数据库或配置读取器)的文件。

  • testsuites 定义了 PHPUnit 查找测试的目录。在我们的例子中,我们定义了 ./tests,但如果在不同的目录中,我们还可以添加更多。

  • whitelist 定义了包含测试代码的目录列表。这有助于生成与代码覆盖率相关的输出。

使用 PHPUnit 运行测试时,只需确保在 phpunit.xml 文件所在的同一目录下运行命令即可。我们将在下一节向你演示如何操作。

第一个测试

好了,准备工作和理论知识到此为止,让我们来编写一些代码吧。我们将为基本客户编写测试,基本客户是一个几乎没有逻辑的领域对象。首先,我们需要重构 Unique trait,因为在将应用程序与 MySQL 集成后,它仍包含一些不必要的代码。我们要讨论的是分配下一个可用 ID 的功能,该功能现在由自动递增字段处理。将其删除,代码如下:

<?php

namespace Bookstore\Utils;

trait Unique
{
    protected $id;

    public function setId(int $id) {
        $this->id = $id;
    }
    public function getId(): int {
        return $this->id;
    }
}

测试将放在 tests/ 目录中。目录的结构应与 src/ 目录相同,以便更容易确定每个测试的位置。文件名和类名必须以 Test 结尾,以便 PHPUnit 知道文件中包含测试。因此,我们的测试应放在 tests/Domain/Customer/BasicTest.php 中,如下所示:

<?php

namespace Bookstore\Tests\Domain\Customer;

use Bookstore\Domain\Customer\Basic;
use PHPUnit_Framework_TestCase;

class BasicTest extends PHPUnit_Framework_TestCase {
    public function testAmountToBorrow() {
        $customer = new Basic(1, 'han', 'solo', 'han@solo.com');

        $this->assertSame(
            3,
            $customer->getAmountToBorrow(),
            'Basic customer should borrow up to 3 books.'
        );
    }
}

正如您所注意到的,BasicTest 类从 PHPUnit_Framework_TestCase 扩展而来。所有测试类都必须从该类扩展而来。该类自带的一组方法可让你做出断言。在 PHPUnit 中,断言只是对一个值进行检查。断言可以是与其他值的比较,也可以是对值的某些属性的验证,等等。如果断言不成立,测试将被标记为失败,并向开发人员输出适当的错误信息。示例展示了一个使用 assertSame 方法的断言,该方法将比较两个值,希望两个值完全相同。第三个参数是断言失败时显示的错误信息。

此外,请注意以 test 开头的函数名是用 PHPUnit 执行的函数。在本例中,我们有一个名为 testAmountToBorrow 的唯一测试,它实例化了一个基本客户,并验证客户可借阅的图书数量为 3。

如果在方法的 DocBlock 中添加 @test 注解,也可以使用任何函数名,如下所示:

/**
* @test
*/
public function thisIsATestToo() {
    //...
}

运行测试

为了运行您编写的测试,您需要执行 Composer 在 vendor/bin 中生成的脚本。记住一定要从项目的根目录运行,这样 PHPUnit 才能找到 phpunit.xml 配置文件。然后,键入 ./vendor/bin/phpunit

image 2023 11 03 18 09 41 930

执行该程序时,我们将得到测试的反馈。输出显示有一个测试(一个方法)和一个断言,以及这些测试是否令人满意。这是你每次运行测试时都希望看到的输出结果,但失败的测试会比你希望的多。让我们添加以下测试来看看:

public function testFail() {
    $customer = new Basic(1, 'han', 'solo', 'han@solo.com');

    $this->assertSame(
        4,
        $customer->getAmountToBorrow(),
        'Basic customer should borrow up to 3 books.'
    );
}

这个测试会失败,因为我们要检查 getAmountToBorrow 是否返回 4,但我们知道它总是返回 3。让我们运行测试,看看会得到什么样的输出结果。

image 2023 11 03 18 11 41 090

我们很快就能注意到,红色的输出结果并不理想。它向我们显示了失败,并指出了失败的类和测试方法。反馈指出了失败的类型(因为 3 和 4 并不相同),还可以选择在调用 assert 方法时添加的错误信息。