xepozz/internal-mocker

此包的最新版本(1.4)没有可用的许可证信息。

1.4 2024-03-01 05:39 UTC

This package is auto-updated.

Last update: 2024-09-04 09:10:27 UTC


README

该包可以帮助尽可能简单地模拟内部PHP函数。当您需要模拟如下函数时使用此包: time()str_contains()rand 等。

Latest Stable Version Total Downloads phpunit

目录

安装

composer require xepozz/internal-mocker --dev

使用

主要思路非常简单:为PHPUnit注册一个监听器,并首先调用Mocker扩展。

注册PHPUnit扩展

PHPUnit 9

  1. 创建新文件 tests/MockerExtension.php
  2. 将以下代码粘贴到创建的文件中
    <?php
    declare(strict_types=1);
    
    namespace App\Tests;
    
    use PHPUnit\Runner\BeforeTestHook;
    use PHPUnit\Runner\BeforeFirstTestHook;
    use Xepozz\InternalMocker\Mocker;
    use Xepozz\InternalMocker\MockerState;
    
    final class MockerExtension implements BeforeTestHook, BeforeFirstTestHook
    {
        public function executeBeforeFirstTest(): void
        {
            $mocks = [];
    
            $mocker = new Mocker();
            $mocker->load($mocks);
            MockerState::saveState();
        }
    
        public function executeBeforeTest(string $test): void
        {
            MockerState::resetState();
        }
    }
  3. phpunit.xml.dist 中注册钩子作为扩展
    <extensions>
        <extension class="App\Tests\MockerExtension"/>
    </extensions>

PHPUnit 10及以上

  1. 创建新文件 tests/MockerExtension.php
  2. 将以下代码粘贴到创建的文件中
    <?php
    declare(strict_types=1);
    
    namespace App\Tests;
    
    use PHPUnit\Event\Test\PreparationStarted;
    use PHPUnit\Event\Test\PreparationStartedSubscriber;
    use PHPUnit\Event\TestSuite\Started;
    use PHPUnit\Event\TestSuite\StartedSubscriber;
    use PHPUnit\Runner\Extension\Extension;
    use PHPUnit\Runner\Extension\Facade;
    use PHPUnit\Runner\Extension\ParameterCollection;
    use PHPUnit\TextUI\Configuration\Configuration;
    use Xepozz\InternalMocker\Mocker;
    use Xepozz\InternalMocker\MockerState;
    
    final class MockerExtension implements Extension
    {
        public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void
        {
            $facade->registerSubscribers(
                new class () implements StartedSubscriber {
                    public function notify(Started $event): void
                    {
                        MockerExtension::load();
                    }
                },
                new class implements PreparationStartedSubscriber {
                    public function notify(PreparationStarted $event): void
                    {
                        MockerState::resetState();
                    }
                },
            );
        }
    
        public static function load(): void
        {
            $mocks = [];
    
            $mocker = new Mocker();
            $mocker->load($mocks);
            MockerState::saveState();
        }
    }
  3. phpunit.xml.dist 中注册钩子作为扩展
    <extensions>
        <bootstrap class="App\Tests\MockerExtension"/>
    </extensions>

这里您已注册了一个扩展,每次您运行 ./vendor/bin/phpunit 时都会调用它。

默认情况下,所有函数都将生成并保存到 /vendor/bin/xepozz/internal-mocker/data/mocks.php 文件中。

通过覆盖 Mocker 构造函数的第一个参数来更改路径

$mocker = new Mocker('/path/to/your/mocks.php');

注册模拟

该包支持几种模拟函数的方法

  1. 运行时模拟
  2. 预定义模拟
  3. 前两种方式的混合

运行时模拟

如果您想使用模拟函数进行测试用例,应先注册它。

回到创建的 MockerExtension::executeBeforeFirstTest 并编辑 $mocks 变量。

$mocks = [
    [
        'namespace' => 'App\Service',
        'name' => 'time',
    ],
];

此模拟将通过生成的包装器代理命名空间 App\Service 下的 time() 的每个调用。

当您想在测试中模拟结果时,应将以下代码写入所需的测试用例中

MockerState::addCondition(
   'App\Service', // namespace
   'time', // function name
   [], // arguments
   100 // result
);

您也可以使用回调来设置函数的结果

MockerState::addCondition(
   '', // namespace
   'headers_sent', // function name
   [null, null], // both arguments are references and they are not initialized yet on the function call
   fn (&$file, &$line) => $file = $line = 123, // callback result
);

因此,您的测试用例将如下所示

<?php
namespace App\Tests;

use App\Service;
use PHPUnit\Framework\TestCase;

class ServiceTest extends TestCase
{
    public function testRun2(): void
    {
        $service = new Service();

        MockerState::addCondition(
            'App\Service',
            'time',
            [],
            100
        );

        $this->assertEquals(100, $service->doSomething());
   }
}

请参阅完整示例 \Xepozz\InternalMocker\Tests\Integration\DateTimeTest::testRun2

预定义模拟

预定义模拟允许您全局模拟行为。

这意味着如果您想为整个项目模拟它,您不需要在每个测试用例中写入 MockerState::addCondition(...)

请注意,来自不同命名空间的相同函数对 Mocker 来说是不同的。

因此,回到创建的 MockerExtension::executeBeforeFirstTest 并编辑 $mocks 变量。

$mocks = [
    [
        'namespace' => 'App\Service',
        'name' => 'time',
        'result' => 150,
        'arguments' => [],
    ],
];

在此变体之后,每个 App\Service\time() 将返回 150

您可以添加很多模拟。 Mocker 比较调用函数的参数值与调用函数的参数,并返回所需的结果。

前两种方式的混合

混合意味着您可以使用 预定义模拟 首先使用,然后使用 运行时模拟

状态

如果您使用 运行时模拟,您可能会遇到问题,即在模拟函数之后,您在另一个测试用例中仍然模拟了它。

MockerState::saveState()MockerState::resetState() 解决了这个问题。

这些方法保存“当前”状态并卸载每个已应用的 运行时模拟 模拟。

使用 MockerState::saveState()Mocker->load($mocks) 之后仅保存 预定义 模拟。

跟踪调用

您可以通过使用MockerState::getTraces()方法来跟踪模拟函数的调用。

$traces = MockerState::getTraces('App\Service', 'time');

$traces将包含具有以下结构的数组数组

[
    [
        'arguments' => [], // arguments of the function
        'trace' => [], // the result of debug_backtrace function
        'result' => 1708764835, // result of the function
    ],
    // ...
]

函数签名占位符

所有内部函数都被模拟以与原始函数兼容。这使得函数使用引用参数(&$file)作为原始函数那样。

它们位于src/stubs.php文件中。

如果您需要添加新的函数签名,则覆盖Mocker构造函数的第二个参数

$mocker = new Mocker(stubPath: '/path/to/your/stubs.php');

全局命名空间函数

内部函数

模拟全局函数的方法是在php.ini中禁用它们:https://php.ac.cn/manual/en/ini.core.php#ini.disable-functions

最好的方法是通过运行带有附加标志的命令仅禁用测试中的函数

php -ddisable_functions=${functions} ./vendor/bin/phpunit

如果您使用PHPStorm,可以在Run/Debug Configurations部分设置命令。将标志-ddisable_functions=${functions}添加到Interpreter options字段。

您可以将命令保留在composer.json文件中的scripts部分。

{
  "scripts": {
    "test": "php -ddisable_functions=time,serialize,header,date ./vendor/bin/phpunit"
  }
}

${functions}替换为您想要模拟的函数列表,用逗号分隔,例如:time,rand

因此,现在您也可以模拟全局函数了。

内部函数实现

当您在php.ini中禁用函数时,您将无法再次调用它。这意味着您必须自己实现它。

显然,几乎所有函数的实现都与Bash中的实现非常相似。

实现函数的最简单方法是用`bash命令`语法

$mocks[] = [
   'namespace' => '',
   'name' => 'time',
   'function' => fn () => `date +%s`,
];

请记住,如果未实施全局函数,则会导致函数的递归调用,这会导致致命错误。

限制

数据提供者

有时,在未强制使用namespace的情况下模拟函数时,您可能会遇到不愉快的情况。

  • 。这可能意味着您正在尝试在@dataProvider中创建PHP解释器文件。请小心,作为一个解决方案,我建议您在测试构造函数中调用模拟器。因此,首先将所有代码从您的扩展方法executeBeforeFirstTest移动到新的静态方法,并在executeBeforeFirstTest__construct方法中调用它。
final class MyTest extends \PHPUnit\Framework\TestCase
{
    public function __construct(?string $name = null, array $data = [], $dataName = '')
    {
        \App\Tests\MockerExtension::load();
        parent::__construct($name, $data, $dataName);
    }
    
    /// ...
}
final class MockerExtension implements BeforeTestHook, BeforeFirstTestHook
{
    public function executeBeforeFirstTest(): void
    {
        self::load();
    }

    public static function load(): void
    {
        $mocks = [];

        $mocker = new Mocker();
        $mocker->load($mocks);
        MockerState::saveState();
    }

    public function executeBeforeTest(string $test): void
    {
        MockerState::resetState();
    }
}

这都是由于PHPUnit 9.5及以下事件管理系统。数据提供程序功能在所有事件之前开始工作,因此不可能在运行开始时模拟函数。