跳到内容

PHPUnit Bridge

编辑此页

PHPUnit Bridge 提供了实用程序来报告遗留测试和已弃用代码的使用情况,以及用于模拟与时间、DNS 和类存在相关的原生函数的助手。

它具有以下功能

  • 默认情况下为您的测试设置一致的区域设置 (C)(如果您创建区域设置敏感的测试,请使用 PHPUnit 的 setLocale() 方法);
  • 自动注册 class_exists 以加载 Doctrine 注解(当使用时);
  • 它显示应用程序中使用的所有已弃用功能的完整列表;
  • 按需显示弃用的堆栈跟踪;
  • 提供 ClockMockDnsMockClassExistsMock 类,用于对时间、网络或类存在敏感的测试;
  • 提供修改后的 PHPUnit 版本,允许

    1. 将您的应用程序的依赖项与 phpunit 的依赖项分离,以防止任何不需要的约束适用;
    2. 当测试套件拆分为多个 phpunit.xml 文件时,并行运行测试;
    3. 记录和重放跳过的测试;
  • 它允许创建与多个 PHPUnit 版本兼容的测试(因为它为缺少的方法提供了 polyfill,为非命名空间类提供了命名空间别名等)。

安装

1
$ composer require --dev symfony/phpunit-bridge

注意

如果您在 Symfony 应用程序外部安装此组件,则必须在您的代码中 require vendor/autoload.php 文件,以启用 Composer 提供的类自动加载机制。阅读本文以获取更多详细信息。

注意

PHPUnit bridge 旨在与所有维护的 Symfony 组件版本一起工作,即使跨越它们的不同主要版本也是如此。您应该始终使用其最新的稳定主要版本,以获得最准确的弃用报告。

如果您计划编写关于弃用的断言并使用常规 PHPUnit 脚本(而不是 Symfony 提供的修改后的 PHPUnit 脚本),您必须注册一个新的测试监听器,名为 SymfonyTestsListener

1
2
3
4
5
6
7
8
9
10
11
<!-- https://phpunit.de/manual/6.0/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/6.0/phpunit.xsd"
>

    <!-- ... -->

    <listeners>
        <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener"/>
    </listeners>
</phpunit>

用法

另请参阅

本文解释了如何在任何 PHP 应用程序中将 PhpUnitBridge 功能用作独立组件。阅读测试文章,了解如何在 Symfony 应用程序中使用它。

一旦组件安装完成,将在 vendor/ 目录中创建一个 simple-phpunit 脚本来运行测试。此脚本包装了原始 PHPUnit 二进制文件以提供更多功能

1
2
$ cd my-project/
$ ./vendor/bin/simple-phpunit

运行 PHPUnit 测试后,您将获得类似于此的报告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ ./vendor/bin/simple-phpunit
  PHPUnit by Sebastian Bergmann.

  Configuration read from <your-project>/phpunit.xml.dist
  .................

  Time: 1.77 seconds, Memory: 5.75Mb

  OK (17 tests, 21 assertions)

  Remaining deprecation notices (2)

  getEntityManager is deprecated since Symfony 2.1. Use getManager instead: 2x
    1x in DefaultControllerTest::testPublicUrls from App\Tests\Controller
    1x in BlogControllerTest::testIndex from App\Tests\Controller

摘要包括

未静默
报告在没有推荐的@-静默运算符的情况下触发的弃用通知。
遗留
弃用通知表示明确测试某些遗留功能的测试。
剩余/其他
弃用通知是所有其他(非遗留)通知,按消息、测试类和方法分组。

注意

如果您不想使用 simple-phpunit 脚本,请在您的 PHPUnit 配置文件中注册以下PHPUnit 事件监听器,以获得关于弃用的相同报告(该报告由一个名为 DeprecationErrorHandlerPHP 错误处理程序创建)

1
2
3
4
5
<!-- phpunit.xml.dist -->
<!-- ... -->
<listeners>
    <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener"/>
</listeners>

并行运行测试

修改后的 PHPUnit 脚本允许通过提供包含多个测试套件及其自己的 phpunit.xml.dist 的目录来并行运行测试。

1
2
3
4
5
6
7
├── tests/
│   ├── Functional/
│   │   ├── ...
│   │   └── phpunit.xml.dist
│   ├── Unit/
│   │   ├── ...
│   │   └── phpunit.xml.dist
1
$ ./vendor/bin/simple-phpunit tests/

修改后的 PHPUnit 脚本将递归遍历提供的目录,深度最多为 3 个子目录或环境变量 SYMFONY_PHPUNIT_MAX_DEPTH 指定的值,查找 phpunit.xml.dist 文件,然后并行运行找到的每个套件,收集它们的输出,并在它们自己的部分中显示每个测试套件的结果。

触发弃用通知

弃用通知可以通过使用 symfony/deprecation-contracts 包中的 trigger_deprecation 来触发

1
2
3
4
5
// indicates something is deprecated since version 1.3 of vendor-name/packagename
trigger_deprecation('vendor-name/package-name', '1.3', 'Your deprecation message');

// you can also use printf format (all arguments after the message will be used)
trigger_deprecation('...', '1.3', 'Value "%s" is deprecated, use ...  instead.', $value);

将测试标记为遗留

有三种方法可以将测试标记为遗留

  • 推荐)将 @group legacy 注解添加到其类或方法;
  • 使其类名以 Legacy 前缀开头;
  • 使其方法名称以 testLegacy*() 而不是 test*() 开头。

注意

如果您的数据提供器调用通常会触发弃用的代码,您可以将其名称前缀添加 provideLegacygetLegacy 以静默这些弃用。如果您的数据提供器不执行已弃用的代码,则不需要选择特殊的命名,仅仅是因为由数据提供器提供的测试被标记为遗留。

还要注意,选择两个遗留前缀之一不会将使用此数据提供器的测试标记为遗留。您仍然必须将它们显式标记为遗留测试。

配置

如果您需要检查单元测试触发的特定弃用的堆栈跟踪,您可以将 SYMFONY_DEPRECATIONS_HELPER 环境变量设置为与此弃用消息匹配的正则表达式,并用 / 括起来。例如,使用

1
2
3
4
5
6
7
8
9
10
11
12
<!-- https://phpunit.de/manual/6.0/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/6.0/phpunit.xsd"
>

    <!-- ... -->

    <php>
        <server name="KERNEL_CLASS" value="App\Kernel"/>
        <env name="SYMFONY_DEPRECATIONS_HELPER" value="/foobar/"/>
    </php>
</phpunit>

PHPUnit 将在触发消息包含 "foobar" 字符串的弃用通知后停止您的测试套件。

使测试失败

默认情况下,任何未标记为遗留或任何未静默(@-静默运算符)的弃用通知都会使测试失败。或者,您可以通过将 SYMFONY_DEPRECATIONS_HELPER 设置为例如 max[total]=320 来配置任意阈值。只有当达到更高数量的弃用通知时(0 是默认值),它才会使测试失败。

您甚至可以通过使用 max 数组的其他键来获得更精细的控制,这些键是 selfdirectindirectSYMFONY_DEPRECATIONS_HELPER 环境变量接受 URL 编码的字符串,这意味着您可以组合阈值和任何其他配置设置,如下所示:SYMFONY_DEPRECATIONS_HELPER='max[total]=42&max[self]=0&verbose=0'

内部弃用

当您维护一个库时,测试套件在依赖项引入新的弃用时立即失败是不理想的,因为它会将修复该弃用的负担转移给在新的供应商发布包含该弃用后不久提交拉取请求的任何贡献者。

为了缓解这种情况,您可以收紧要求,希望依赖项不会在补丁版本中引入弃用,甚至可以提交 composer.lock 文件,这会产生另一类问题。库通常会使用 SYMFONY_DEPRECATIONS_HELPER=max[total]=999999,因为这样。这有一个缺点,即允许引入弃用的贡献,但是

  • 忘记修复已弃用的调用(如果有);
  • 忘记用 @group legacy 注解标记适当的测试。

通过使用 SYMFONY_DEPRECATIONS_HELPER=max[self]=0,在 vendor/ 目录外部触发的弃用将被单独计算,而从其中的库触发的弃用将不会(除非您达到 999999 个),从而为您提供两全其美的效果。

直接和间接弃用

在项目上工作时,您可能对 max[direct] 更感兴趣。假设您希望在弃用出现后立即修复它们。许多开发人员遇到的一个问题是,他们拥有的一些依赖项往往落后于他们自己的依赖项,这意味着他们不会尽快修复弃用,这意味着您应该在过时的供应商上创建一个拉取请求,并忽略这些弃用,直到您的拉取请求被合并。

max[direct] 配置允许您仅对直接弃用设置阈值,使您注意到您的代码何时在使用已弃用的 API,并跟上更改。如果您想将间接弃用保持在给定阈值以下,您仍然可以使用 max[indirect]

这是一个摘要,应该可以帮助您选择正确的配置

推荐情况
max[total]=0 推荐用于具有强大/无依赖项的积极维护的项目
max[direct]=0 推荐用于依赖项未能跟上新弃用的项目。
max[self]=0 推荐用于自己使用弃用系统并且无法负担使用上述模式之一的库。

忽略弃用

如果您的应用程序有一些由于某些原因无法修复的弃用,您可以告诉 Symfony 忽略它们。

您首先需要创建一个文本文件,其中每行都是要忽略的弃用,定义为正则表达式。以井号 (#) 开头的行被视为注释

1
2
3
4
5
# This file contains patterns to be ignored while testing for use of
# deprecated code.

%The "Symfony\\Component\\Validator\\Context\\ExecutionContextInterface::.*\(\)" method is considered internal Used by the validator engine\. (Should not be called by user\W+code\. )?It may change without further notice\. You should not extend it from "[^"]+"\.%
%The "PHPUnit\\Framework\\TestCase::addWarning\(\)" method is considered internal%

然后,您可以运行以下命令来使用该文件并忽略这些弃用

1
$ SYMFONY_DEPRECATIONS_HELPER='ignoreFile=./tests/baseline-ignore' ./vendor/bin/simple-phpunit

基线弃用

您还可以拍摄当前由您的应用程序代码触发的弃用的快照,并在测试运行期间忽略这些弃用,但仍然报告新添加的弃用。诀窍是创建一个包含允许的弃用的文件,并将其定义为“弃用基线”。该文件中的弃用将被忽略,但其余的弃用仍将报告。

首先,生成包含允许的弃用的文件(每当您想要更新现有文件时运行相同的命令)

1
$ SYMFONY_DEPRECATIONS_HELPER='generateBaseline=true&baselineFile=./tests/allowed.json' ./vendor/bin/simple-phpunit

此命令将运行测试时报告的所有弃用存储在给定的文件路径中,并以 JSON 编码。

然后,您可以运行以下命令来使用该文件并忽略这些弃用

1
$ SYMFONY_DEPRECATIONS_HELPER='baselineFile=./tests/allowed.json' ./vendor/bin/simple-phpunit

禁用详细输出

默认情况下,bridge 将显示详细的输出,其中包含弃用的数量以及它们出现的位置。如果这对您来说太多了,您可以使用 SYMFONY_DEPRECATIONS_HELPER=verbose=0 来关闭详细输出。

也可以更改每种弃用类型的详细程度。例如,使用 quiet[]=indirect&quiet[]=other 将隐藏类型为“indirect”和“other”的弃用的详细信息。

quiet 选项隐藏指定弃用类型的详细信息,但不会更改退出代码方面的结果。这就是 max 的用途,并且这两个设置是正交的。

禁用弃用助手

SYMFONY_DEPRECATIONS_HELPER 环境变量设置为 disabled=1 以完全禁用弃用助手。这对于利用此组件提供的其余功能而不会收到与弃用相关的错误或消息很有用。

自动加载时的弃用通知

默认情况下,PHPUnit Bridge 使用来自 ErrorHandler 组件DebugClassLoader,以在类自动加载时抛出弃用通知。可以使用 debug-class-loader 选项禁用此功能。

1
2
3
4
5
6
7
8
9
10
11
12
<!-- phpunit.xml.dist -->
<!-- ... -->
<listeners>
    <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener">
        <arguments>
            <array>
                <!-- set this option to 0 to disable the DebugClassLoader integration -->
                <element key="debug-class-loader"><integer>0</integer></element>
            </array>
        </arguments>
    </listener>
</listeners>

编译时弃用

使用 debug:container 命令列出容器编译和预热期间生成的弃用

1
$ php bin/console debug:container --deprecations

记录弃用

要关闭详细输出并将其写入日志文件,您可以改用 SYMFONY_DEPRECATIONS_HELPER='logFile=/path/deprecations.log'

为测试设置区域设置

默认情况下,PHPUnit Bridge 强制将区域设置设置为 C,以避免测试中的区域设置问题。可以通过将 SYMFONY_PHPUNIT_LOCALE 环境变量设置为所需的区域设置来更改此行为

1
2
# .env.test
SYMFONY_PHPUNIT_LOCALE="fr_FR"

或者,您可以在 PHPUnit 配置文件中设置此环境变量

1
2
3
4
5
6
7
8
9
10
11
12
<!-- https://phpunit.de/manual/6.0/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/6.0/phpunit.xsd"
>

    <!-- ... -->

    <php>
        <!-- ... -->
        <env name="SYMFONY_PHPUNIT_LOCALE" value="fr_FR"/>
    </php>
</phpunit>

最后,如果您想避免 bridge 强制任何区域设置,可以将 SYMFONY_PHPUNIT_LOCALE 环境变量设置为 0

编写关于弃用的断言

向代码中添加弃用时,您可能希望编写测试来验证它们是否按需触发。为此,bridge 提供了 expectDeprecation() 方法,您可以在测试方法中使用它。它要求您传递期望的消息,格式与 PHPUnit 的 assertStringMatchesFormat() 方法相同。如果您期望给定的测试方法有多个弃用消息,您可以多次使用该方法(顺序很重要)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;

class MyTest extends TestCase
{
    use ExpectDeprecationTrait;

    /**
     * @group legacy
     */
    public function testDeprecatedCode(): void
    {
        // test some code that triggers the following deprecation:
        // trigger_deprecation('vendor-name/package-name', '5.1', 'This "Foo" method is deprecated.');
        $this->expectDeprecation('Since vendor-name/package-name 5.1: This "%s" method is deprecated');

        // ...

        // test some code that triggers the following deprecation:
        // trigger_deprecation('vendor-name/package-name', '4.4', 'The second argument of the "Bar" method is deprecated.');
        $this->expectDeprecation('Since vendor-name/package-name 4.4: The second argument of the "%s" method is deprecated.');
    }
}

显示完整堆栈跟踪

默认情况下,PHPUnit Bridge 仅显示弃用消息。要显示与弃用相关的完整堆栈跟踪,请将 SYMFONY_DEPRECATIONS_HELPER 的值设置为与弃用消息匹配的正则表达式。

例如,如果抛出以下弃用通知

1
2
1x: Doctrine\Common\ClassLoader is deprecated.
  1x in EntityTypeTest::setUp from Symfony\Bridge\Doctrine\Tests\Form\Type

运行以下命令将显示完整的堆栈跟踪

1
$ SYMFONY_DEPRECATIONS_HELPER='/Doctrine\\Common\\ClassLoader is deprecated\./' ./vendor/bin/simple-phpunit

使用多个 PHPUnit 版本进行测试

当测试一个必须与多个 PHP 版本兼容的库时,测试套件不能使用最新版本的 PHPUnit,因为

  • PHPUnit 8 弃用了一些方法,转而使用旧版本(例如 PHPUnit 4)中不可用的其他方法;
  • PHPUnit 8 将 void 返回类型添加到 setUp() 方法,这与 PHP 5.5 不兼容;
  • PHPUnit 从 PHPUnit 6 开始切换到命名空间类,因此测试必须在有和没有命名空间的情况下都能工作。

不可用方法的 Polyfill

当使用 simple-phpunit 脚本时,PHPUnit Bridge 为 TestCaseAssert 类的大多数方法注入了 polyfill(例如 expectException()expectExceptionMessage()assertContainsEquals() 等)。这允许编写使用最新最佳实践的测试用例,同时仍然与旧版本的 PHPUnit 兼容。

移除 Void 返回类型

当使用设置了 SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT 环境变量为 1simple-phpunit 脚本时,PHPUnit bridge 将修改 PHPUnit 的代码,以从 setUp()tearDown()setUpBeforeClass()tearDownAfterClass() 方法中删除返回类型(在 PHPUnit 8 中引入)。这允许您编写与 PHP 5 和 PHPUnit 8 都兼容的测试。

使用命名空间的 PHPUnit 类

PHPUnit bridge 为大多数未声明命名空间的 PHPUnit 类(例如 PHPUnit_Framework_Assert)添加了命名空间类别名,允许您始终使用命名空间类声明,即使测试是在 PHPUnit 4 中执行的。

时间敏感的测试

用例

如果您有这种与时间相关的测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use PHPUnit\Framework\TestCase;
use Symfony\Component\Stopwatch\Stopwatch;

class MyTest extends TestCase
{
    public function testSomething(): void
    {
        $stopwatch = new Stopwatch();

        $stopwatch->start('event_name');
        sleep(10);
        $duration = $stopwatch->stop('event_name')->getDuration();

        $this->assertEquals(10000, $duration);
    }
}

您使用 Stopwatch 实用程序计算了流程的持续时间,以 分析 Symfony 应用程序的性能。但是,根据服务器的负载或本地计算机上运行的进程,$duration 可能例如是 10.000023s 而不是 10s

这种测试称为瞬态测试:它们会随机失败,具体取决于虚假的外部环境。当使用像 Travis CI 这样的公共持续集成服务时,它们通常会引起麻烦。

时钟模拟

此 bridge 提供的 ClockMock 类允许您模拟 PHP 的内置时间函数 time()microtime()sleep()usleep()gmdate()hrtime()。此外,date() 函数也被模拟,因此如果未指定时间戳,它将使用模拟时间。

其他带有可选时间戳参数(默认为 time())的函数仍将使用系统时间而不是模拟时间。这意味着您可能需要在测试中更改一些代码。例如,您应该使用 DateTime::createFromFormat('U', (string) time()) 而不是 new DateTime() 来使用模拟的 time() 函数。

要在测试中使用 ClockMock 类,请向其类或方法添加 @group time-sensitive 注解。此注解仅在使用 vendor/bin/simple-phpunit 脚本执行 PHPUnit 或在 PHPUnit 配置中注册以下监听器时才有效

1
2
3
4
5
<!-- phpunit.xml.dist -->
<!-- ... -->
<listeners>
    <listener class="\Symfony\Bridge\PhpUnit\SymfonyTestsListener"/>
</listeners>

注意

如果您不想使用 @group time-sensitive 注解,您可以手动注册 ClockMock 类,方法是在测试之前调用 ClockMock::register(__CLASS__)ClockMock::withClockMock(true),并在测试之后调用 ClockMock::withClockMock(false)

因此,以下内容保证可以正常工作,不再是瞬态测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use PHPUnit\Framework\TestCase;
use Symfony\Component\Stopwatch\Stopwatch;

/**
 * @group time-sensitive
 */
class MyTest extends TestCase
{
    public function testSomething(): void
    {
        $stopwatch = new Stopwatch();

        $stopwatch->start('event_name');
        sleep(10);
        $duration = $stopwatch->stop('event_name')->getDuration();

        $this->assertEquals(10000, $duration);
    }
}

就这样!

警告

基于时间的函数模拟遵循 PHP 命名空间解析规则,因此无法模拟“完全限定的函数调用”(例如 \time())。

@group time-sensitive 注解等效于调用 ClockMock::register(MyTest::class)。如果您想模拟在不同类中使用的函数,请使用 ClockMock::register(MyClass::class) 显式执行此操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// the class that uses the time() function to be mocked
namespace App;

class MyClass
{
    public function getTimeInHours(): void
    {
        return time() / 3600;
    }
}

// the test that mocks the external time() function explicitly
namespace App\Tests;

use App\MyClass;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ClockMock;

/**
 * @group time-sensitive
 */
class MyTest extends TestCase
{
    public function testGetTimeInHours(): void
    {
        ClockMock::register(MyClass::class);

        $my = new MyClass();
        $result = $my->getTimeInHours();

        $this->assertEquals(time() / 3600, $result);
    }
}

提示

使用 ClockMock 类的一个额外好处是时间会立即流逝。使用 PHP 的 sleep(10) 将使您的测试等待 10 秒的实际时间(或多或少)。相反,ClockMock 类会将内部时钟提前给定的秒数,而无需实际等待那么长时间,因此您的测试将快 10 秒执行。

DNS 敏感的测试

进行网络连接的测试,例如检查 DNS 记录的有效性,执行速度可能很慢,并且由于网络状况而不可靠。因此,此组件还为以下 PHP 函数提供了模拟:

用例

考虑以下示例,该示例测试了一个名为 DomainValidator 的自定义类,该类定义了一个 checkDnsRecord 选项,用于同时验证域名是否与有效主机关联

1
2
3
4
5
6
7
8
9
10
11
12
13
use App\Validator\DomainValidator;
use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    public function testEmail(): void
    {
        $validator = new DomainValidator(['checkDnsRecord' => true]);
        $isValid = $validator->validate('example.com');

        // ...
    }
}

为了避免建立真正的网络连接,请将 @group dns-sensitive 注解添加到类中,并使用 DnsMock::withMockedHosts() 来配置您期望从给定主机获得的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use App\Validator\DomainValidator;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\DnsMock;

/**
 * @group dns-sensitive
 */
class DomainValidatorTest extends TestCase
{
    public function testEmails(): void
    {
        DnsMock::withMockedHosts([
            'example.com' => [['type' => 'A', 'ip' => '1.2.3.4']],
        ]);

        $validator = new DomainValidator(['checkDnsRecord' => true]);
        $isValid = $validator->validate('example.com');

        // ...
    }
}

withMockedHosts() 方法配置定义为一个数组。键是被模拟的主机,值是 DNS 记录数组,格式与 dns_get_record 返回的格式相同,因此您可以模拟各种网络状况

1
2
3
4
5
6
7
8
9
10
11
12
DnsMock::withMockedHosts([
    'example.com' => [
        [
            'type' => 'A',
            'ip' => '1.2.3.4',
        ],
        [
            'type' => 'AAAA',
            'ipv6' => '::12',
        ],
    ],
]);

基于类存在的测试

根据现有类(例如 Composer 的开发依赖项)表现不同的测试通常很难测试备用情况。因此,此组件还为以下 PHP 函数提供了模拟:

用例

考虑以下示例,该示例依赖于 Vendor\DependencyClass 来切换行为

1
2
3
4
5
6
7
8
9
10
11
12
13
use Vendor\DependencyClass;

class MyClass
{
    public function hello(): string
    {
        if (class_exists(DependencyClass::class)) {
            return 'The dependency behavior.';
        }

        return 'The default behavior.';
    }
}

MyClass 的常规测试用例(假设在测试期间安装了开发依赖项)将如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
use MyClass;
use PHPUnit\Framework\TestCase;

class MyClassTest extends TestCase
{
    public function testHello(): void
    {
        $class = new MyClass();
        $result = $class->hello(); // "The dependency behavior."

        // ...
    }
}

为了测试默认行为,请使用 ClassExistsMock::withMockedClasses() 来配置代码要运行的预期类、接口和/或 trait

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use MyClass;
use PHPUnit\Framework\TestCase;
use Vendor\DependencyClass;

class MyClassTest extends TestCase
{
    // ...

    public function testHelloDefault(): void
    {
        ClassExistsMock::register(MyClass::class);
        ClassExistsMock::withMockedClasses([DependencyClass::class => false]);

        $class = new MyClass();
        $result = $class->hello(); // "The default behavior."

        // ...
    }
}

请注意,使用 ClassExistsMock::withMockedClasses() 模拟类将使 class_existsinterface_existstrait_exists 返回 true。

要注册枚举并模拟 enum_exists,必须使用 ClassExistsMock::withMockedEnums()。请注意,与 PHP 8.1 及更高版本中一样,在枚举上调用 class_exists 将返回 true。这就是为什么调用 ClassExistsMock::withMockedEnums() 也会将枚举注册为模拟类。

故障排除

@group time-sensitive@group dns-sensitive 注解“按约定”工作,并假设可以通过从测试命名空间中删除 Tests\ 部分来获得被测类的命名空间。即,如果您的测试用例完全限定类名 (FQCN) 是 App\Tests\Watch\DummyWatchTest,则它假定被测类的命名空间是 App\Watch

如果此约定不适用于您的应用程序,请在 phpunit.xml 文件中配置模拟命名空间,例如在 HttpKernel 组件中所做的那样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- https://phpunit.de/manual/4.1/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/4.1/phpunit.xsd"
>

    <!-- ... -->

    <listeners>
        <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener">
            <arguments>
                <array>
                    <element key="time-sensitive"><string>Symfony\Component\HttpFoundation</string></element>
                </array>
            </arguments>
        </listener>
    </listeners>
</phpunit>

在底层,PHPUnit 监听器将被模拟函数注入到被测类的命名空间中。为了按预期工作,监听器必须在被测类运行之前运行。

默认情况下,模拟函数在找到注解并运行相应的测试时创建。根据您的测试构建方式,这可能为时已晚。

您可以选择

  • 在您的 phpunit.xml.dist 中声明被测类的命名空间;
  • config/bootstrap.php 文件的末尾注册命名空间。
1
2
3
4
5
6
7
8
9
10
11
<!-- phpunit.xml.dist -->
<!-- ... -->
<listeners>
    <listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener">
            <arguments>
                <array>
                    <element key="time-sensitive"><string>Acme\MyClassTest</string></element>
                </array>
            </arguments>
        </listener>
</listeners>
1
2
3
4
5
6
7
// config/bootstrap.php
use Symfony\Bridge\PhpUnit\ClockMock;

// ...
if ('test' === $_SERVER['APP_ENV']) {
    ClockMock::register('Acme\\MyClassTest\\');
}

修改后的 PHPUnit 脚本

此 bridge 提供了一个修改版的 PHPUnit,您可以使用其 bin/simple-phpunit 命令调用它。它具有以下功能

  • 使用独立的 vendor 目录,该目录与您的目录不冲突;
  • 不嵌入 prophecy,以防止与其依赖项发生任何冲突;
  • 当定义了 SYMFONY_PHPUNIT_SKIPPED_TESTS 环境变量时,收集和重放跳过的测试:env 变量应指定一个文件名,该文件名将用于在第一次运行时存储跳过的测试,并在第二次运行时重放它们;
  • 当给定目录作为参数时,并行化测试套件执行,扫描此目录中最多 SYMFONY_PHPUNIT_MAX_DEPTH 级别(指定为 env 变量,默认为 3)的 phpunit.xml.dist 文件;

该脚本将其构建的修改版 PHPUnit 写入一个目录,该目录可以通过 SYMFONY_PHPUNIT_DIR env 变量配置,或者如果未提供该变量,则写入与 simple-phpunit 相同的目录。也可以在 phpunit.xml.dist 文件中设置此 env 变量。

如果您已通过 Composer 安装了 bridge,则可以通过调用例如以下命令来运行它:

1
$ vendor/bin/simple-phpunit

提示

可以通过在 phpunit.xml.dist 文件中设置 SYMFONY_PHPUNIT_VERSION env 变量来更改 PHPUnit 版本(例如 <server name="SYMFONY_PHPUNIT_VERSION" value="5.5"/>)。这是首选方法,因为它可以提交到您的版本控制存储库。

也可以将 SYMFONY_PHPUNIT_VERSION 设置为真正的 env 变量(未在 dotenv 文件中定义)。

以相同的方式,SYMFONY_MAX_PHPUNIT_VERSION 将设置要考虑的 PHPUnit 的最大版本。当测试不支持最新版本 PHPUnit 的框架时,这非常有用。

提示

如果您仍然需要使用 prophecy(但不是 symfony/yaml),则将 SYMFONY_PHPUNIT_REMOVE env 变量设置为 symfony/yaml

也可以在 phpunit.xml.dist 文件中设置此 env 变量。

提示

还可以使用 SYMFONY_PHPUNIT_REQUIRE env 变量来请求其他软件包,这些软件包将与其余所需的 PHPUnit 软件包一起安装。这对于安装 PHPUnit 插件特别有用,而无需将它们添加到您的主 composer.json 文件中。所需的软件包需要用空格分隔。

1
2
3
4
5
<!-- phpunit.xml.dist -->
<!-- ... -->
<php>
    <env name="SYMFONY_PHPUNIT_REQUIRE" value="vendor/name:^1.2 vendor/name2:^3"/>
</php>

代码覆盖率监听器

默认情况下,代码覆盖率是使用以下规则计算的:如果执行了一行代码,则将其标记为已覆盖。因此,执行一行代码的测试被标记为“覆盖该行代码”。这可能会产生误导。

考虑以下示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Bar
{
    public function barMethod(): string
    {
        return 'bar';
    }
}

class Foo
{
    public function __construct(
        private Bar $bar,
    ) {
    }

    public function fooMethod(): string
    {
        $this->bar->barMethod();

        return 'bar';
    }
}

class FooTest extends PHPUnit\Framework\TestCase
{
    public function test(): void
    {
        $bar = new Bar();
        $foo = new Foo($bar);

        $this->assertSame('bar', $foo->fooMethod());
    }
}

FooTest::test 方法执行了 FooBar 类的每一行代码,但 Bar 并没有真正被测试。CoverageListener 旨在通过在每个测试类上添加适当的 @covers 注解来修复此行为。

如果测试类已经定义了 @covers 注解,则此监听器不执行任何操作。否则,它会尝试通过删除类名的 Test 部分来查找与测试相关的代码:My\Namespace\Tests\FooTest -> My\Namespace\Foo

安装

将以下配置添加到 phpunit.xml.dist 文件

1
2
3
4
5
6
7
8
9
10
11
<!-- https://phpunit.de/manual/6.0/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/6.0/phpunit.xsd"
>

    <!-- ... -->

    <listeners>
        <listener class="Symfony\Bridge\PhpUnit\CoverageListener"/>
    </listeners>
</phpunit>

如果用于查找相关代码的逻辑过于简单或不适用于您的应用程序,则可以使用您自己的 SUT(被测系统)求解器

1
2
3
4
5
6
7
<listeners>
    <listener class="Symfony\Bridge\PhpUnit\CoverageListener">
        <arguments>
            <string>My\Namespace\SutSolver::solve</string>
        </arguments>
    </listener>
</listeners>

My\Namespace\SutSolver::solve 可以是任何 PHP 可调用对象,并接收当前测试作为其第一个参数。

最后,当 SUT 求解器找不到 SUT 时,监听器还可以显示警告消息

1
2
3
4
5
6
7
8
<listeners>
    <listener class="Symfony\Bridge\PhpUnit\CoverageListener">
        <arguments>
            <null/>
            <boolean>true</boolean>
        </arguments>
    </listener>
</listeners>
这项工作,包括代码示例,根据 Creative Commons BY-SA 3.0 许可获得许可。
TOC
    版本