yoast/phpunit-polyfills

一组用于修改 PHPUnit 功能的 polyfills,以便创建 PHPUnit 跨版本兼容的测试

3.0.0 2024-09-07 00:24 UTC

README

Version CS Build Status Lint Build Status Test Build Status Coverage Status

Minimum PHP Version License: BSD3

一组用于修改 PHPUnit 功能的 polyfills,以便创建 PHPUnit 跨版本兼容的测试。

要求

  • PHP 7.0 或更高版本。
  • PHPUnit 6.4 - 9.x 和 11.x(通过 Composer 自动要求)。

安装

要安装此软件包,请运行

composer require --dev yoast/phpunit-polyfills:"^3.0"

要更新此软件包,请运行

composer update --dev yoast/phpunit-polyfills --with-dependencies

自动加载

请确保

  • 要么使用 Composer 的 vendor/autoload.php 文件作为您的测试引导文件;
  • 要么在您的测试引导中引入 vendor/yoast/phpunit-polyfills/phpunitpolyfills-autoload.php 文件。

为什么要使用 PHPUnit Polyfills?

此库通过提供一组 polyfills 来允许创建 PHPUnit 跨版本兼容的测试,这些 polyfills 用于 PHPUnit 中引入、拆分或重命名的功能。

为 PHPUnit 11.x 编写测试并在 PHPUnit 6.4 - 11.x 上运行

polyfills 已配置为允许测试向前兼容。这意味着,您的测试可以使用最新 PHPUnit 版本支持的断言,即使在旧版本的 PHPUnit 上运行。

这将在您想要开始在较新版本上运行测试时,将升级到较新 PHPUnit 版本的负担降到最低。通过这样做,取消对旧版本 PHPUnit 的支持就像从您的 composer.json 文件中的版本约束中删除它一样简单。

PHPUnit 支持

  • PHPUnit Polyfills 的 1.x 系列版本支持 PHPUnit 4.8 - 9.x。
  • PHPUnit Polyfills 的 2.x 系列版本支持 PHPUnit 5.7 - 10.x。
  • PHPUnit Polyfills 的 3.x 系列版本支持 PHPUnit 6.4 - 11.x(但不支持在 PHPUnit 10 上运行测试)。

请注意,PHPUnit Polyfills 提供了向前兼容性。这意味着,PHPUnit 在 10.x 中不再支持的功能(如期望 PHP 弃用通知或警告)将不会在 PHPUnit Polyfills 2.x 系列中支持,且在 11.x 中不受支持的功能将不会在 3.x 系列中支持。

请参阅 PHPUnit 10 发布通知/PHPUnit 10 变更日志PHPUnit 11 发布通知/PHPUnit 11 变更日志,以了解是否升级(尚未升级)。

使用此库

每个 polyfill 和辅助函数都已配置为 trait,可以在任何扩展 PHPUnit 原生 TestCase 类的测试文件中导入和 use

如果正在运行的测试不需要特定版本的PHPUnit的polyfill,自动加载器将自动加载一个同名但为空的特性,因此您可以安全地在需要PHPUnit跨版本兼容的测试中使用这些特性。

<?php

namespace Vendor\YourPackage\Tests;

use PHPUnit\Framework\TestCase;
use Yoast\PHPUnitPolyfills\Polyfills\AssertIsType;

class FooTest extends TestCase
{
    use AssertIsType;

    public function testSomething()
    {
        $this->assertIsBool( $maybeBool );
        self::assertIsNotIterable( $maybeIterable );
    }
}

或者,您可以使用该库提供的TestCase,而不是使用PHPUnit原生的TestCase类。

在这种情况下,所有polyfill和辅助工具将在需要时可用。

<?php

namespace Vendor\YourPackage\Tests;

use Yoast\PHPUnitPolyfills\TestCases\TestCase;

class FooTest extends TestCase
{
    public function testSomething()
    {
        $this->assertIsBool( $maybeBool );
        self::assertMatchesRegularExpression( $pattern, $string, $message );
    }
}

支持的断言调用方式

默认情况下,PHPUnit支持四种调用断言的方式

  1. 作为TestCase类中的方法 - $this->assertSomething()
  2. 作为TestCase类中的静态方法 - self/static/parent::assertSomething()
  3. 作为Assert类的静态方法 - Assert::assertSomething()
  4. 作为全局函数 - assertSomething()

此库中的polyfill支持前两种调用断言的方式,因为这些是最常用的断言调用类型。

为了让polyfill正常工作,测试类必须是一个(外)孙类PHPUnit原生TestCase类。

与 PHPUnit < 7.5.0 一起使用

如果您的库仍然需要支持PHP < 7.1,因此需要PHPUnit < 7进行测试,那么在单独使用特性时有一些注意事项,因为我们进入了“双重polyfill”领域。

为了防止在类中多次使用特性时出现“冲突的方法名称”错误,这里提供的特性不会尝试解决这个问题。

您需要确保使用polyfill正常工作所需的任何附加特性。

注意:这仅适用于特性的独立使用。该库提供的TestCase已经自动处理了这个问题。

使用AssertIgnoringLineEndings特性的测试代码示例,该特性需要在PHPUnit 6.4上运行

<?php

namespace Vendor\YourPackage\Tests;

use PHPUnit\Framework\TestCase;
use Yoast\PHPUnitPolyfills\Polyfills\AssertIgnoringLineEndings;
use Yoast\PHPUnitPolyfills\Polyfills\AssertStringContains;

class FooTest extends TestCase
{
    use AssertIgnoringLineEndings;
    use AssertStringContains;

    public function testSomething()
    {
        $this->assertStringContainsStringIgnoringLineEndings(
            "something\nelse",
            "this is something\r\nelse"
        );
    }
}

特性

Polyfill 特性

PHPUnit < 7.5.0: Yoast\PHPUnitPolyfills\Polyfills\AssertIsType

polyfill以下方法

这些方法是在PHPUnit 7.5.0中引入的,作为对软弃用的Assert::assertInternalType()Assert::assertNotInternalType()方法以及硬弃用(警告)在PHPUnit 8.0.0中并从PHPUnit 9.0.0中删除的替代。

PHPUnit < 7.5.0: Yoast\PHPUnitPolyfills\Polyfills\AssertStringContains

polyfill以下方法

这些方法是在PHPUnit 7.5.0中引入的,作为使用字符串作为堆栈的Assert::assertContains()Assert::assertNotContains()的替代。将这些方法作为字符串堆栈传递被软弃用,并在PHPUnit 8.0.0中硬弃用(警告),在PHPUnit 9.0.0中删除。

PHPUnit < 7.5.0: Yoast\PHPUnitPolyfills\Polyfills\AssertEqualsSpecializations

polyfill以下方法

这些方法是在PHPUnit 7.5.0中引入的,作为使用这些可选参数的Assert::assertEquals()Assert::assertNotEquals()的替代。将这些可选参数传递给这些方法被软弃用,并在PHPUnit 8.0.0中硬弃用(警告),在PHPUnit 9.0.0中删除。

PHPUnit < 8.4.0: Yoast\PHPUnitPolyfills\Polyfills\ExpectExceptionMessageMatches

polyfillTestCase::expectExceptionMessageMatches()方法。

此方法是在PHPUnit 8.4.0中引入的,以改进TestCase::expectExceptionMessageRegExp()方法的名称。该方法在PHPUnit 8.4.0中软弃用,在PHPUnit 8.5.3中硬弃用(警告),在PHPUnit 9.0.0中删除。

PHPUnit < 8.5.0: Yoast\PHPUnitPolyfills\Polyfills\AssertFileEqualsSpecializations

polyfill以下方法

这些方法是在PHPUnit 8.5.0中引入的,作为使用这些可选参数的Assert::assertFileEquals()Assert::assertFileNotEquals()的替代。将这些可选参数传递给这些方法在PHPUnit 8.5.0中被硬弃用,并在PHPUnit 9.0.0中删除。

PHPUnit < 9.0.0: Yoast\PHPUnitPolyfills\Polyfills\EqualToSpecializations

polyfill以下方法

这些方法通常用于验证传递给Mock对象的参数,它们在PHPUnit 9.0.0版本中被引入,作为使用Assert::EqualTo()和可选参数的替代方案。在PHPUnit 9.0.0中移除了对将相应可选参数传递给Assert::EqualTo()的支持。

PHPUnit < 9.1.0: Yoast\PHPUnitPolyfills\Polyfills\AssertionRenames

实现了以下重命名的方法

这些方法在PHPUnit 9.1.0中被引入。这些新方法所替代的原始方法在PHPUnit 9.1.0中被严格弃用,并在PHPUnit 10.0.0中被移除。

PHPUnit < 9.3.0: Yoast\PHPUnitPolyfills\Polyfills\AssertClosedResource

polyfill以下方法

这些方法在PHPUnit 9.3.0中被引入。

此外,这个特质还包含一个辅助方法shouldClosedResourceAssertionBeSkipped()

由于PHP自身存在一些bug,"是否为关闭的资源"的确定不能总是可靠地进行,尤其是对于libxml扩展。

这个辅助函数可以确定当前"待测试的值"以及运行测试的PHP版本是否受这些bug的影响。

⚠️ 这些断言的PHPUnit本地实现也受到这些bug的影响!因此,shouldClosedResourceAssertionBeSkipped()辅助方法在跨版本中可用。

使用示例

// Example: skipping the test completely.
if ( $this->shouldClosedResourceAssertionBeSkipped( $actual ) === true ) {
    $this->markTestSkipped('assertIs[Not]ClosedResource() cannot determine whether this resource is'
        . ' open or closed due to bugs in PHP (PHP ' . \PHP_VERSION . ').');
}

// Example: selectively skipping the assertion.
if ( self::shouldClosedResourceAssertionBeSkipped( $actual ) === false ) {
    $this->assertIsClosedResource( $actual );
}

👉 虽然这个polyfill经过了广泛的测试,但全面测试这类bug 非常困难。请报告任何发现的bug,并包含一个清晰的代码示例以重现问题。

PHPUnit < 9.4.0: Yoast\PHPUnitPolyfills\Polyfills\AssertObjectEquals

实现了Assert::assertObjectEquals()方法,以验证两个(值)对象被认为是相等的。这个断言期望对象本身包含一个比较方法。随后调用此比较方法以验证对象的"相等性"。

assertObjectEquals()断言在PHPUnit 9.4.0中被引入。

ℹ️ 在PHPUnit Polyfills 1.x和2.x版本中,此断言的polyfill存在一些限制。[查看详情](https://github.com/Yoast/PHPUnit-Polyfills/?tab=readme-ov-file#phpunit--940-yoastphpunitpolyfillspolyfillsassertobjectequals)。这些限制在PHPUnit Polyfills 3.x版本中已不再存在,断言现在与PHPUnit原生实现匹配。

PHPUnit < 10.0.0: Yoast\PHPUnitPolyfills\Polyfills\AssertIgnoringLineEndings

polyfill以下方法

这些方法是在PHPUnit 10.0.0中引入的。

PHPUnit < 10.0.0: Yoast\PHPUnitPolyfills\Polyfills\AssertIsList

polyfills以下方法

此方法是在PHPUnit 10.0.0中引入的。

PHPUnit < 10.1.0: Yoast\PHPUnitPolyfills\Polyfills\AssertObjectProperty

polyfill以下方法

这些方法是在PHPUnit 10.1.0中引入的,作为对Assert::assertObjectHasAttribute()Assert::assertObjectNotHasAttribute()方法的替代,这些方法在PHPUnit 9.6.1中被硬弃用(警告),并在PHPUnit 10.0.0中删除。

这些方法后来被回滚到PHPUnit 9分支中,并包含在PHPUnit 9.6.11版本中。

PHPUnit < 11.0.0: Yoast\PHPUnitPolyfills\Polyfills\AssertArrayWithListKeys

polyfill以下方法

这些方法是在PHPUnit 11.0.0中引入的。

此功能类似于之前由Assert::assertArraySubset()断言提供的功能,该断言在PHPUnit 9.0.0中删除,但精度更高。

建议将仍然使用Assert::assertArraySubset()的测试重构为使用新断言,作为升级路径。

PHPUnit < 11.0.0: Yoast\PHPUnitPolyfills\Polyfills\ExpectUserDeprecation

这些方法是在PHPUnit 11.0.0中引入的。

此功能类似于之前由TestCase::expectDeprecationMessage()TestCase::expectDeprecationMessageMatches()方法提供的功能,这些方法在PHPUnit 10.0.0中被删除。

polyfill在PHPUnit <= 9版本下使用旧方法,但新旧方法之间有一些重要的行为差异,polyfill的用户应该注意。

在编写使用expectUserDeprecationMessage*()方法的测试时,请记住这些差异。

注意:在PHPUnit 9.5.x版本中,当使用expectUserDeprecationMessage*()期望时,测试输出将显示"Expecting E_DEPRECATED and E_USER_DEPRECATED is deprecated and will no longer be possible in PHPUnit 10."弃用警告。只要实际测试使用expectUserDeprecationMessage*()期望,则可以安全地忽略此弃用消息。

ℹ️ 重要:当在测试中使用expectUserDeprecationMessage*()期望时,测试应使用#[IgnoreDeprecations]属性。

PHPUnit < 11.2.0: Yoast\PHPUnitPolyfills\Polyfills\AssertObjectNotEquals

polyfillsAssert::assertObjectNotEquals()方法以验证两个(值)对象不被认为是相等的。这是PHPUnit 9.4+ Assert::assertObjectEquals()方法的姊妹方法。

此断言期望对象自身包含一个比较方法。然后调用此比较方法来验证对象的"相等性"。

assertObjectNotEquals()断言是在PHPUnit 11.2.0中引入的。

测试用例

PHPUnit 8.0.0将void返回类型声明引入到"fixture"方法 - setUpBeforeClass()setUp()tearDown()tearDownAfterClass()。由于void返回类型直到PHP 7.1才引入,因此使用fixture时,由于签名不匹配,创建跨版本兼容的测试变得困难。

此库包含两个基本的TestCase选项来解决这个问题。

选项1: Yoast\PHPUnitPolyfills\TestCases\TestCase

TestCase通过具有两个版本来克服签名不匹配。正确的一个将根据所使用的PHPUnit版本被加载。

在使用此 TestCase 时,如果某个测试用例或扩展此 TestCase 的其他 TestCase 需要重载任何“fixture”方法,它应该通过使用原始 fixture 方法名的 snake_case 变体来实现,即 set_up_before_class()set_up()assert_pre_conditions()assert_post_conditions()tear_down()tear_down_after_class()

snake_case 方法将被 PHPUnit 自动调用。

重要: snake_case 方法不应该调用 PHPUnit 父类,即不要在重载的 set_up() 方法中使用 parent::setUp()。如果需要,可以调用 parent::set_up()

use Yoast\PHPUnitPolyfills\TestCases\TestCase;

class MyTest extends TestCase {
    public static function set_up_before_class() {
        parent::set_up_before_class();

        // Set up a database connection or other fixture which needs to be available.
    }

    protected function set_up() {
        parent::set_up();

        // Set up function mocks which need to be available for all tests in this class.
    }

    protected function assert_pre_conditions() {
        parent::assert_pre_conditions();

        // Perform assertions shared by all tests of a test case (before the test).
    }

    protected function assert_post_conditions() {
        // Performs assertions shared by all tests of a test case (after the test).

        parent::assert_post_conditions();
    }

    protected function tear_down() {
        // Any clean up needed related to `set_up()`.

        parent::tear_down();
    }

    public static function tear_down_after_class() {
        // Close database connection and other clean up related to `set_up_before_class()`.

        parent::tear_down_after_class();
    }
}

选项 2: Yoast\PHPUnitPolyfills\TestCases\XTestCase

TestCase 通过使用 PHPUnit 的 @before[Class]@after[Class] 注解以及不同的方法名来克服签名不匹配,例如 setUpFixturesBeforeClass()setUpFixtures()tearDownFixtures()tearDownFixturesAfterClass()

使用此 TestCase 时,重载的 fixture 方法需要使用 @beforeClass@before@after@afterClass 注解。只要方法名不与 PHPUnit 的原生方法名冲突,重载方法的命名是开放的。

use Yoast\PHPUnitPolyfills\TestCases\XTestCase;

class MyTest extends XTestCase {
    /**
     * @beforeClass
     */
    public static function setUpFixturesBeforeClass() {
        parent::setUpFixturesBeforeClass();

        // Set up a database connection or other fixture which needs to be available.
    }

    /**
     * @before
     */
    protected function setUpFixtures() {
        parent::setUpFixtures();

        // Set up function mocks which need to be available for all tests in this class.
    }

    /**
     * @after
     */
    protected function tearDownFixtures() {
        // Any clean up needed related to `setUpFixtures()`.

        parent::tearDownFixtures();
    }

    /**
     * @afterClass
     */
    public static function tearDownFixturesAfterClass() {
        // Close database connection and other clean up related to `setUpFixturesBeforeClass()`.

        parent::tearDownFixturesAfterClass();
    }
}

测试监听器

⚠️ 重要 ⚠️

PHPUnit Polyfills 2.0/3.0 中的 TestListener polyfill 与 (尚不)兼容 PHPUnit 10.x/11.x

如果您需要 TestListener polyfill,建议您暂时停留在 PHPUnit Polyfills 1.x 系列,并关注并点赞 相关票据

以下文档是为 PHPUnit 6.x-9.x TestListener polyfill 实现的。

PHPUnit TestListener 接口的方法签名在版本之间已经更改多次。此外,PHPUnit 7 中已经弃用了 TestListener 原则,转而使用 TestRunner 钩子接口

注意:虽然已弃用,但 TestListener 接口尚未被移除,并在 PHPUnit 9.x 中仍得到支持。

如果您测试套件不需要支持 PHPUnit < 7,则强烈建议使用 TestRunner 钩子接口扩展。

但是,对于仍然需要支持 PHPUnit 6 或更低版本的测试套件,实现 TestListener 接口是唯一可行的选择。

Yoast\PHPUnitPolyfills\TestListeners\TestListenerDefaultImplementation

TestListenerDefaultImplementation trait 通过具有多个版本并根据所使用的 PHPUnit 版本加载正确的一个来克服签名不匹配。

TestCase 实现类似,使用没有类型声明的 snake_case 方法来解决签名不匹配问题。snake_case 方法将被自动调用。

TestListener 接口的实现可能使用以下任何一种模式

// PHPUnit 6.
class MyTestListener extends \PHPUnit\Framework\BaseTestListener {}

// PHPUnit 7+.
class MyTestListener implements \PHPUnit\Framework\TestListener {
    use \PHPUnit\Framework\TestListenerDefaultImplementation;
}

用这些替换

use PHPUnit\Framework\TestListener;
use Yoast\PHPUnitPolyfills\TestListeners\TestListenerDefaultImplementation;

class MyTestListener implements TestListener {
    use TestListenerDefaultImplementation;

    // Implement any of the snakecase methods, for example:
    public function add_error( $test, $e, $time ) {
        // Do something when PHPUnit encounters an error.
    }
}

常见问题解答

问:此包是否填充了从 PHPUnit 中删除的功能?

一般来说,删除的功能不会在此包中填充。

对于常用的已删除 PHPUnit 功能,可以提供“辅助工具”。这些 辅助工具 仅作为临时解决方案,以使用户有更多时间重构其测试,使其远离已删除的功能。

没有 PHPUnit 原生替代品的已删除功能

问:此库可以在通过 PHPUnit Phar 文件运行测试时使用吗?

是的,此包也可以用于通过 PHPUnit Phar 文件运行测试。

在这种情况下,请确保在测试引导文件中明确 requirephpunitpolyfills-autoload.php 文件。(当使用或 require Composer 的 vendor/autoload.php 文件作为测试引导时,则不需要这样做。)

问:通过 GitHub Actions 的 setup-php 动作安装库后,如何运行我的测试?

shivammathur/setup-php 版本 2.15.0 以来,PHPUnit Polyfills 可作为可以直接由 Setup-PHP GitHub action 运行器安装的工具之一。

- name: Setup PHP with tools
  uses: shivammathur/setup-php@v2
  with:
    php-version: '8.0'
    tools: phpunit-polyfills

上述步骤将安装 PHPUnit Polyfills 和 PHPUnit,作为 Composer 全局包。

执行此步骤后,您可以使用 phpunit 运行 PHPUnit,就像您通常做的那样。

- name: Run tests
  run: phpunit

👉 如果您依赖 Composer 来自动加载项目文件,您仍然需要运行 composer dump-autoload --dev,并在您的测试引导中包含项目本地的 vendor/autoload.php 文件。

🎓 为什么这样做会起作用

Composer 将所有文件放置在系统路径中全局 Composer bin 目录,并安装的 Composer PHPUnit 版本将加载 Composer 全局的 autoload.php 文件,这将自动加载 PHPUnit Polyfills。

现在您可能会想,“如果我明确在 tools 中请求 phpunitphpunit-polyfills 呢?”

在这种情况下,当您运行 phpunit 时,PHPUnit PHAR 将不知道如何定位 PHPUnit Polyfills,因此您需要在测试引导中进行一些魔法操作才能使其工作。

问:我如何验证所使用的 PHPUnit Polyfills 库版本?

对于复杂的测试设置,例如当 Polyfills 通过测试套件依赖项提供,或可能通过项目顶层加载,能够检查用于测试套件的包版本是否符合要求是有用的。

从版本 1.0.1 开始,PHPUnit Polyfills 的 Autoload 类包含一个可用于此目的的版本号。

通常此类检查会在测试套件引导文件中执行,可能看起来像这样

if ( class_exists( '\Yoast\PHPUnitPolyfills\Autoload' ) === false ) {
    require_once 'vendor/yoast/phpunit-polyfills/phpunitpolyfills-autoload.php';
}

$versionRequirement = '1.0.1';
if ( defined( '\Yoast\PHPUnitPolyfills\Autoload::VERSION' ) === false
    || version_compare( \Yoast\PHPUnitPolyfills\Autoload::VERSION, $versionRequirement, '<' )
) {
    echo 'Error: Version mismatch detected for the PHPUnit Polyfills.',
        ' Please ensure that PHPUnit Polyfills ', $versionRequirement,
        ' or higher is loaded.', PHP_EOL;
    exit(1);
} else {
    echo 'Error: Please run `composer update -W` before running the tests.' . PHP_EOL;
    echo 'You can still use a PHPUnit phar to run them,',
        ' but the dependencies do need to be installed.', PHP_EOL;
    exit(1);
}

问:为什么 PHPUnit Polyfills 3.x 版本不支持在 PHPUnit 10 上运行测试?

PHPUnit 11.0 引入了 expectUserDeprecationMessage*() 方法。为了在 PHPUnit 10 中填充这些方法,意味着 Polyfills 包将不再是一个“即插即用”的辅助包,而是在与 PHPUnit 10 一起使用时需要对使用 Polyfills 的测试套件设置额外的要求(如挂钩到事件或强制使用此包提供的 TestCase)。

鉴于填充这些方法的愿望足够强烈,PHPUnit Polyfills 3.x 分支的发行版不支持在 PHPUnit 10 上运行测试。

这种妥协的影响很小,因为在最常见的情况下,使用 Composer 安装的依赖项运行测试时,这仅仅意味着在 PHP 8.1 上将使用 PHPUnit 9 而不是 PHPUnit 10。没有其他影响。

请注意,PHPUnit 10 中添加的功能仍然在 PHPUnit Polyfills 3.x 中填充并可用。

贡献

欢迎为此项目做出贡献。克隆存储库,从应用补丁的最旧的 #.x 分支分叉,进行更改,提交更改,然后向正确的 #.x 分支发送拉取请求。

如果您不确定您提出的更改是否会受到欢迎,请首先打开一个问题来讨论您的提案。

许可证

此代码在 BSD-3-Clause License 下发布。