m3m0r7 / method-injector
该库增强了测试中对方法的模拟。
Requires
- php: >=7.3
- nikic/php-parser: ^4.3
Requires (Dev)
- brainmaestro/composer-git-hooks: ^2.8
- friendsofphp/php-cs-fixer: ^2.16
- phpunit/phpunit: ^9.1
- squizlabs/php_codesniffer: ^3.5
README
MethodInjector
是一个开源软件项目,为生成目标类中方法、字段和常量的测试替身提供强大的支持。例如,MethodInjector
可以执行以下操作。
- 用模拟函数或匿名函数替换方法中的函数,并执行它。
- 重写指定字段的默认值
- 通过重写常量的值进行测试
- 模拟特定方法的返回值
- 可以在方法执行的开始和结束处插入处理过程
MethodInjector
解析原始类文件并重建类。因此,即使原始类不可继承(即定义了final
),也可以轻松创建测试替身。然而,也可以继承原始类,并通过继承声明期望的类来创建测试替身。
文档
- 日语
- 英语
DEMO
快速入门
可以从以下位置安装。
composer require --dev m3m0r7/method-injector
如何使用?
简单示例
要使用MethodInjector
创建测试替身,请执行以下操作
<?php require __DIR__ . '/vendor/autoload.php'; use MethodInjector\Condition; use MethodInjector\Inspector; use MethodInjector\MethodInjector; $test = \MethodInjector\MethodInjector::factory(); $test ->inspect( Test::class, function (Inspector $inspector) { return $inspector ->methodGroup( '*', function (Condition $condition) { return $condition ->replaceFunction( 'date', function (...$args) { return '2012-01-01'; } ); } ); } ) ->patch(); $mock = $test->createMock(Test::class);
调用factory
方法返回一个MethodInjector
实例。inspect
的第一个参数是创建测试替身的类名,第二个参数可以指定创建验证和测试替身的条件,第三个参数可以指定是否继承原始类。您还可以调用多个inspect
方法,一次创建多个类的测试替身。
<?php require __DIR__ . '/vendor/autoload.php'; use MethodInjector\Condition; use MethodInjector\Inspector; use MethodInjector\MethodInjector; $test = \MethodInjector\MethodInjector::factory(); $inspector = function (Inspector $inspector) { return $inspector ->methodGroup( '*', function (Condition $condition) { return $condition ->replaceFunction( 'date', function (...$args) { return '2012-01-01'; } ); } ); }; $test ->inspect( Foo::class, $inspector ) ->inspect( Bar::class, $inspector ) ->patch(); $fooMock = $test->createMock(Foo::class); $barMock = $test->createMock(Bar::class);
因为测试替身是在MethodInjector
的专用命名空间中生成的,所以基本上不会污染全局命名空间。
限制替换方法
您还可以通过指定方法名来指定并限制Inspector
类的MethodInjector
要替换的方法。方法名不区分大小写,根据PHP规范。
<?php require __DIR__ . '/vendor/autoload.php'; use MethodInjector\Condition; use MethodInjector\Inspector; use MethodInjector\MethodInjector; $test = \MethodInjector\MethodInjector::factory(); $test ->inspect( Test::class, function (Inspector $inspector) { return $inspector ->methodGroup( 'test', function (Condition $condition) { return $condition ->replaceFunction( 'date', function (...$args) { return '2012-01-01'; } ); } ); } ) ->patch(); $mock = $test->createMock(Test::class);
替换类中的字段
您可能想重写类中字段的默认值。您还可以使用replaceField
来更改字段的默认值。当然,即使字段是private
或protected
,也可以更改字段。
<?php require __DIR__ . '/vendor/autoload.php'; use MethodInjector\Condition; use MethodInjector\Inspector; use MethodInjector\MethodInjector; $test = \MethodInjector\MethodInjector::factory(); $test ->inspect( Test::class, function (Inspector $inspector) { return $inspector ->replaceField('testField', 'changed default value'); } ) ->patch(); $mock = $test->createMock(Test::class); echo $mock->testField;
替换类中的常量
您可以使用replaceConstant
以与重写字段相同的方式重写常量值。当然,这也适用于private
,甚至protected
。
<?php require __DIR__ . '/vendor/autoload.php'; use MethodInjector\Condition; use MethodInjector\Inspector; use MethodInjector\MethodInjector; $test = \MethodInjector\MethodInjector::factory(); $test ->inspect( Test::class, function (Inspector $inspector) { return $inspector ->replaceConstant('TEST', 'changed default value'); } ) ->patch(); $mock = $test->createMock(Test::class); echo $mock::TEST;
在开始处理方法时输出某些内容
您可能想在方法执行开始时中断一些处理。使用MethodInjector
,您可以指定Condition
类的before
。在执行开始时,可以中断一些处理。例如,当您想测量单个方法的执行时间时,这很有用。
<?php require __DIR__ . '/vendor/autoload.php'; use MethodInjector\Condition; use MethodInjector\Inspector; use MethodInjector\MethodInjector; $test = \MethodInjector\MethodInjector::factory(); $test ->inspect( Test::class, function (Inspector $inspector) { return $inspector ->methodGroup( '*', function (Condition $condition) { return $condition ->before(function () { echo "Hello HEAD!\n"; }); } ); } ) ->patch(); $mock = $test->createMock(Test::class);
在完成处理方法时输出某些内容
您也可以指定方法执行的结束,就像开始一样。当它完成后,将调用after
。
<?php require __DIR__ . '/vendor/autoload.php'; use MethodInjector\Condition; use MethodInjector\Inspector; use MethodInjector\MethodInjector; $test = \MethodInjector\MethodInjector::factory(); $test ->inspect( Test::class, function (Inspector $inspector) { return $inspector ->methodGroup( '*', function (Condition $condition) { return $condition ->after(function () { echo "Finish to run.\n"; }); } ); } ) ->patch(); $mock = $test->createMock(Test::class);
替换方法中的类
MethodInjector
还可以使用 replaceInstance
方法替换方法中实例化的类为另一个类。这允许使用还在开发中的用例,或者当你想要替换一个类为另一个类时。你还可以在读取时使用 static
或 self
来替换它。
<?php require __DIR__ . '/vendor/autoload.php'; use MethodInjector\Condition; use MethodInjector\Inspector; use MethodInjector\MethodInjector; $test = \MethodInjector\MethodInjector::factory(); $test ->inspect( Test::class, function (Inspector $inspector) { return $inspector ->methodGroup( '*', function (Condition $condition) { return $condition ->replaceInstance( Foo::class, Bar::class ); } ); } ) ->patch(); $mock = $test->createMock(Test::class);
当然,即使你正在读取一个类的静态方法,你也可以使用 replaceStaticCall
来替换它。
<?php require __DIR__ . '/vendor/autoload.php'; use MethodInjector\Condition; use MethodInjector\Inspector; use MethodInjector\MethodInjector; $test = \MethodInjector\MethodInjector::factory(); $test ->inspect( Test::class, function (Inspector $inspector) { return $inspector ->methodGroup( '*', function (Condition $condition) { return $condition ->replaceStaticCall( Foo::class, Bar::class ); } ); } ) ->patch(); $mock = $test->createMock(Test::class);
模拟方法
当方法本身还在开发中,或者当你想要测试一个返回特定值的测试,或者它不在你的测试兴趣范围内时,你可能想要创建一个测试替身。使用 MethodInjector
,你也可以通过 replaceMethod
创建一个用于该方法本身的测试替身。
<?php require __DIR__ . '/vendor/autoload.php'; use MethodInjector\Condition; use MethodInjector\Inspector; use MethodInjector\MethodInjector; $test = \MethodInjector\MethodInjector::factory(); $test ->inspect( Test::class, function (Inspector $inspector) { return $inspector ->replaceMethod( 'testMethod', function () { return 'Fixed value.'; } ); } ) ->patch(); $mock = $test->createMock(Test::class);
添加替换器
MethodInjector
预先提供了几个替换器,但根据情况你可能想要添加更多的替换器。在这种情况下,你也可以添加替换器。默认情况下,MethodInjector
的替换器是设置为默认的,但如果添加了原始的替换器,请注意,MethodInjector
提供的播放器默认情况下不会被使用,所以它应该作为参数重新指定。
<?php require __DIR__ . '/vendor/autoload.php'; use MethodInjector\Condition; use MethodInjector\Inspector; use MethodInjector\MethodInjector; $test = \MethodInjector\MethodInjector::factory([ 'replacer' => [ [Inspector::FUNCTION, MyFunctionReplacer::class], [Inspector::CLASS_CONSTANT, MyConstantReplacer::class], ], ]); $test ->inspect( Test::class, function (Inspector $inspector) { return $inspector ->replaceMethod( 'testMethod', function () { return 'Fixed value.'; } ); } ) ->patch(); $mock = $test->createMock(Test::class);
如果你使用 addReplacer
,你可以直接使用 MethodInjector
提供的替换器。替换器的作用类似于 reduce,按照指定的顺序应用于解析的 AST 节点。
<?php require __DIR__ . '/vendor/autoload.php'; use MethodInjector\Condition; use MethodInjector\Inspector; use MethodInjector\MethodInjector; $test = \MethodInjector\MethodInjector::factory(); $test ->inspect( Test::class, function (Inspector $inspector) { return $inspector ->addReplacer(Inspector::FUNCTION, MyFunctionReplacer::class) ->addReplacer(Inspector::CLASS_CONSTANT, MyConstantReplacer::class); } ) ->patch();
更改检查器
MethodInjector
的检查器只提供了所需的最基本功能,并且可能存在障碍,例如无法用于具有历史的项目。在这种情况下,你可能想要替换检查器本身以便进行测试。当然,你也可以实现原始的检查器。在这种情况下,你应该扩展由 MethodInjector
默认提供的 Inspector
。
<?php require __DIR__ . '/vendor/autoload.php'; use MethodInjector\Condition; use MethodInjector\Inspector; use MethodInjector\MethodInjector; $test = \MethodInjector\MethodInjector::factory([ 'inspectorClass' => MyInspector::class, ]); $test ->inspect( Test::class, function (Inspector $inspector) { return $inspector ->addReplacer(Inspector::FUNCTION, MyFunctionReplacer::class) ->addReplacer(Inspector::CLASS_CONSTANT, MyConstantReplacer::class); } ) ->patch();
模拟类或特质中定义的常量、字段和方法。
MethodInjector
对父类中定义的方法和字段以及特质进行静态分析,并将它们分解为 AST 并模拟它们。这个特性允许你专注于编写眼前的测试,而无需考虑在创建测试替身时如何模拟父类和特质的类方法。此外,由于 MethodInjector
递归地引用类,因此可以模拟父类以及父类的父类,或者如果是特质,则可以模拟特质中定义的特质。
<?php require __DIR__ . '/vendor/autoload.php'; use MethodInjector\Condition; use MethodInjector\Inspector; use MethodInjector\MethodInjector; $test = \MethodInjector\MethodInjector::factory(); $test ->inspect( Test::class, function (Inspector $inspector) { return $inspector ->enableParentMock(true) ->enableTraitsMock(true); } ) ->patch();
构建器
如果你正在使用 MethodInjector
,你可能觉得创建模拟所需的代码量很长。因此,MethodInjector
提供了一个对象,一个名为 ConditionBuilder
的构建器类,以便更容易地创建对象。默认情况下,ConditionBuilder
模拟父类和所有特质的方法和属性。
<?php use MethodInjector\Builder\ConditionBuilder; $builder = ConditionBuilder::factory(Test::class) ->replaceFunction( 'date', function (...$args) { return '9999-99-99'; } ) ->make() ->patch();
你可以编写上面的内容。上面的内容等同于以下内容。
<?php $test = \MethodInjector\MethodInjector::factory(); $test ->inspect( Test::class, function (Inspector $inspector) { return $inspector ->methodGroup( '*', function (Condition $condition) { return $condition ->replaceFunction( 'date', function (...$args) { return '9999-99-99'; } ); } ) ->enableParentMock(true) ->enableTraitsMock(true); } ) ->patch();
ConditionBuilder
默认应用的方法范围是 *
,即所有方法。然而,你可能只想模拟一些方法。在这种情况下,你可以使用 group
来指定要模拟的方法。
<?php use MethodInjector\Builder\ConditionBuilder; $builder = ConditionBuilder::factory(Test::class) ->group('doSomething') ->replaceFunction( 'date', function (...$args) { return '9999-99-99'; } ) ->make() ->patch();
在这种情况下,所有 doSomething
中的 date
函数都被模拟了。
<?php use MethodInjector\Builder\ConditionBuilder; $builder = ConditionBuilder::factory(Test::class) ->group('doSomething1') ->replaceFunction( 'date', function (...$args) { return '9999-99-99'; } ) ->group('doSomething2') ->replaceFunction( 'date', function (...$args) { return '0000-00-00'; } ) ->make() ->patch();
在上面的情况下,使用 doSomething1
执行的日期将返回 9999-99-99
,而使用 doSomething2
执行的日期将返回 0000-00-00
。
许可证
MIT