mazer / rewrite-dagger
一款PHP dagger,通过重写代码来为测试切割出“缝隙”,而不是使用任何如runkit的扩展。
Requires
- php: ^7.2
Requires (Dev)
- friendsofphp/php-cs-fixer: *
- phpunit/phpunit: ^8 || ^9
This package is auto-updated.
Last update: 2024-09-08 15:39:01 UTC
README
一款不使用任何扩展的PHP测试工具,可以模拟任何东西。
目录
安装
composer require mazer/rewrite-dagger
特性
RewriteDagger可以模拟测试目标所依赖的任何东西。无论这些函数和类是来自PHP内置、第三方还是您的项目。通过在包含和评估测试目标代码之前重写测试目标代码,RewriteDagger可以在不使用任何扩展的情况下替换测试目标中存在的任何单词和内容。
用法
快速开始
以下是需要测试的PHP函数
- header set
- 输出JSON
- 退出
<?php function apiErrorResponse(string $errorMsg): void { header('Content-type: application/json; charset=utf-8'); echo(json_encode([ 'data' => [ 'error' => true, 'message' => $errorMsg ] ])); exit(); }
此函数难以测试,因为它包含一个内置的PHP函数(header),该函数难以感知输入,并且包含一个PHP语言结构(exit),该结构会终止脚本执行。
为了解决这个问题,我们使用PHPUnit和RewriteDagger进行测试。
<?php declare(strict_types=1); use PHPUnit\Framework\TestCase; use RewriteDagger\DaggerFactory; // Mock class that can sense and save function result class Mock { static $exitHasCalled = false; static $header = ''; static public function exit(): void { self::$exitHasCalled = true; } static public function header(string $header): void { self::$header = $header; } } final class ApiErrorResponseTest extends TestCase { public function testApiErrorResponse(): void { $dagger = (new DaggerFactory())->getDagger(); // add rewrite rule $dagger->addReplaceRule('exit', 'Mock::exit'); $dagger->addReplaceRule('header', 'Mock::header'); // include apiErrorResponse function $dagger->includeCode(__DIR__ . '/apiErrorResponse.php'); // call test function ob_start(); apiErrorResponse('test error message'); $output = ob_get_clean(); // assert expect and actual value $this->assertTrue(Mock::$exitHasCalled); $this->assertSame('Content-type: application/json; charset=utf-8', Mock::$header); $this->assertSame('{"data":{"error":true,"message":"test error message"}}', $output); } }
$ vendor/bin/phpunit ApiErrorResponseTest.php
PHPUnit 9.4.0 by Sebastian Bergmann and contributors.
Api Error Response
✔ Api error response
Time: 00:00.015, Memory: 6.00 MB
OK (1 test, 3 assertions)
有了RewriteDagger,我们可以轻松地将header和exit替换为其他可以感知输入且不会终止脚本执行的类。
工作原理
如特性所述,RewriteDagger在包含和评估测试目标代码之前重写测试目标代码。
为了实现此功能,它有三个核心部分
- Dagger:重写测试目标代码
- 代码仓库:包含和评估测试目标代码
- DaggerFactory:创建Dagger
Dagger主要关注各种重写规则本身,并使用DaggerFactory注入的代码仓库来操作、包含和评估代码。
(由skanaar/nomnoml绘制)
接下来,我们将分别解释这三个组件的用法。
Dagger
__construct(CodeRepositoryInterface $codeRepository)
Dagger依赖于任何实现CodeRepositoryInterface的实现来帮助它评估重写的代码。
includeCode(String $path): void
包含、重写、评估与$path对应的代码文件。
Dagger可以有多个重写规则。当调用includeCode时,Dagger会在评估之前对这些规则执行所有操作。
addDeleteRule(String $from): void
$dagger->addDeleteRule('is a number.');
testAddRegexDeleteRule(String $from): void
$dagger->addRegexDeleteRule('/\d+/');
addReplaceRule(String $from, String $to): void
$dagger->addReplaceRule('is a number', ': Answer to the Ultimate Question of Everything');
addRegexReplaceRule(String $from, String $to): void
$dagger->addRegexReplaceRule('/\d+/', 'Number');
addInsertBeforeRule(String $from, String $to): void
$dagger->addInsertBeforeRule('number', 'answer and ');
addRegexInsertBeforeRule(String $from, String $to): void
$dagger->addRegexInsertBeforeRule('/\d+/', '(Number) ');
addInsertAfterRule(String $from, String $to): void
$dagger->addInsertAfterRule('number', ' and answer');
addRegexInsertAfterRule(String $from, String $to): void
$dagger->addRegexInsertAfterRule('/\d+/', ' (Number)');
addRegexReplaceCallbackRule(String $from, callable $callback): void
$dagger->addRegexReplaceCallbackRule('/^(\d+).*(number)\.$/', function ($match) { return "[{$match[1]}] is a ({$match[2]})."; });
testRemoveAllRules(): void
移除之前设置的规则。
代码仓库
所有代码仓库都是CodeRepositoryInterface的实现,提供以下功能
getCodeContent(string $path): string:获取与$path对应的代码内容。includeCode(string $codeContent): void:评估$codeContent。
在PHP中,有两种方法将字符串作为代码进行评估。一种是把字符串写成一个真正的文件,然后使用include()或require()包含并评估它,另一种是使用eval()函数。RewriteDagger分别在FileCodeRepository和EvalCodeRepository中实现了这两种方法。
(由skanaar/nomnoml绘制)
文件代码仓库
__construct(string $tempPath = null)
FileCodeRepository会将字符串写入$tempPath指定路径下的一个临时文件,并使用唯一的文件名,然后包含并评估它。如果$tempPath为null,FileCodeRepository将自动使用sys_get_temp_dir()生成它。
- IncludeFileCodeRepository:使用
include()包含并评估文件。 - RequireFileCodeRepository:使用
require()包含并评估文件。
评估代码仓库
__construct()
EvalCodeRepository比FileCodeRepository简单得多,它直接使用eval()评估输入字符串。
DaggerFactory
通常,除非您想在Dagger中使用自定义的CodeRepository,否则Dagger和CodeRepository通常由DaggerFactory创建,而不是手动创建。
getDagger(array $config = []): Dagger
<?php // default is use IncludeFileCodeRepository $dagger = (new DaggerFactory())->getDagger(); // explicit use IncludeFileCodeRepository $dagger = (new DaggerFactory())->getDagger([ 'codeRepositoryType' => 'include', 'tempPath' => 'your/temp/path/' ]);
initDagger(Dagger $dagger): Dagger
initDagger是一个受保护的函数,它允许您自定义DaggerFactory,可以在返回它之前对Dagger进行操作(主要是添加默认规则)。
<?php class CustomDaggerFactory extends DaggerFactory { protected function initDagger(Dagger $dagger): Dagger { $dagger->addDeleteRule('exit()'); return $dagger; } } // all dagger create by CustomDaggerFactory has a delete exit() rule by default $dagger = (new CustomDaggerFactory())->getDagger();
测试
所有测试命令都在composer.json中定义。
需要注意的是,composer.lock中的phpunit版本是9,它不支持php 7.2。但RewriteDagger支持php 7.2,所以如果您使用的是php 7.2,请确保在测试前运行compoesr update来将phpunit版本更改为8。
{
// ...
"scripts": {
"test": "phpunit",
"testWithCoverage": "phpunit --coverage-text --whitelist src/ --colors",
"codingStyleCheck": "php-cs-fixer fix ./ --dry-run --diff"
},
// ...
}
无覆盖率测试
composer test
有覆盖率测试
composer testWithCoverage
检查代码风格
composer codingStyleCheck
缺点
使用RewriteDagger的两大缺点是测试覆盖率降低和可读性下降。
- 测试覆盖率:由于重写的代码不属于项目中原始代码,对于大多数测试覆盖率工具来说,项目中的原始代码实际上并未被执行。
- 可读性:阅读测试程序的人必须理解所有测试目标,才能理解每个重写规则的副作用。
启发
RewriteDagger受到了书籍《Working Effectively with Legacy Code》的启发(ISBN 13: 978-0131177055)。希望为PHP提供链接接口,使遗留PHP代码更容易进行测试。
相关仓库
许可
MIT许可证