xepozz / internal-mocker
Requires
- yiisoft/var-dumper: ^1.2
Requires (Dev)
- phpunit/phpunit: ^9.5
README
该包可以帮助尽可能简单地模拟内部PHP函数。当您需要模拟如下函数时使用此包: time()
、str_contains()
、rand
等。
目录
安装
composer require xepozz/internal-mocker --dev
使用
主要思路非常简单:为PHPUnit注册一个监听器,并首先调用Mocker扩展。
注册PHPUnit扩展
PHPUnit 9
- 创建新文件
tests/MockerExtension.php
- 将以下代码粘贴到创建的文件中
<?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(); } }
- 在
phpunit.xml.dist
中注册钩子作为扩展<extensions> <extension class="App\Tests\MockerExtension"/> </extensions>
PHPUnit 10及以上
- 创建新文件
tests/MockerExtension.php
- 将以下代码粘贴到创建的文件中
<?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(); } }
- 在
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');
注册模拟
该包支持几种模拟函数的方法
- 运行时模拟
- 预定义模拟
- 前两种方式的混合
运行时模拟
如果您想使用模拟函数进行测试用例,应先注册它。
回到创建的 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及以下事件管理系统。数据提供程序功能在所有事件之前开始工作,因此不可能在运行开始时模拟函数。