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及以下事件管理系统。数据提供程序功能在所有事件之前开始工作,因此不可能在运行开始时模拟函数。