lucatume / function-mocker
使用 Patchwork 进行函数模拟
Requires
- php: >=5.6.0
- antecedent/patchwork: ^2.0
- lucatume/args: ^1.0
- phpunit/phpunit: >=5.7
Requires (Dev)
- phpunit/phpunit: ^5.7
- dev-master
- v2.0.x-dev
- 1.3.8
- 1.3.7
- 1.3.6
- 1.3.5
- 1.3.4
- 1.3.3
- 1.3.2
- 1.3.1
- 1.3.0
- 1.2.2
- 1.2.1
- 1.2.0
- 1.1.0
- 1.0.5
- 1.0.4
- 1.0.3
- 1.0.2
- 1.0.1
- 1.0.0
- 0.4.5
- 0.4.4
- 0.4.3
- 0.4.2
- 0.4.1
- 0.4.0
- 0.3.0
- 0.2.17
- 0.2.16
- 0.2.15
- 0.2.14
- 0.2.13
- 0.2.12
- 0.2.11
- 0.2.10
- 0.2.9
- 0.2.8
- 0.2.7
- 0.2.6
- 0.2.5
- 0.2.4
- 0.2.3
- 0.2.2
- 0.2.1
- 0.2.0
- 0.1.1
- 0.1.0
- dev-test/php-8
This package is auto-updated.
Last update: 2024-09-13 20:04:56 UTC
README
A Patchwork 驱动的函数模拟器。
显示代码
这可以写入一个 PHPUnit 测试套件中
<?php // FunctionMocker needs the functions to be defined to replace them function get_option($option) { // no-op } function update_option($option, $value) { // no-op } // The class under test class Logger { public function log($type, $message) { $option = get_option('log'); $option[] = sprintf('[%s] %s - %s', date(DATE_ATOM, time()), $type, $message); update_option('log', sprintf('[%s] %s - %s', date(DATE_ATOM, time()), $type, $message)); } } class InternaFunctionReplacementTest extends \PHPUnit\Framework\TestCase { /** * It should log the correct message * @test */ public function log_the_correct_message() { $mockTime = time(); \tad\FunctionMocker\FunctionMocker::replace('time', $mockTime); \tad\FunctionMocker\FunctionMocker::replace('get_option', []); $update_option = \tad\FunctionMocker\FunctionMocker::replace('update_option'); $logger = new Logger(); $logger->log('error', 'There was an error'); $expected = sprintf('[%s] error - There was an error', date(DATE_ATOM, $mockTime)); $update_option->wasCalledWithOnce(['log', $expected]); } }
安装
可以将压缩文件移动到相应的文件夹,或者使用 Composer
composer require lucatume/function-mocker:~1.0
用法
在理想的世界里,你永远不需要模拟静态方法和函数,应该使用 TDD 编写更好的面向对象代码,并将其用作设计工具。
但有时可能需要模拟这些函数和静态方法,这个库就是为了帮助您。
初始化
为了使 Function Mocker 充分发挥其封装能力(由 patchwork 授予的能力),需要在 Codeception 或 PHPUnit 的正确引导文件中调用 FunctionMocker::init
方法,如下所示
<?php // This is global bootstrap for autoloading use tad\FunctionMocker\FunctionMocker; require_once dirname( __FILE__ ) . '/../vendor/autoload.php'; FunctionMocker::init(['blacklist' => dirname(__DIR__)]);
init
方法将接受一个配置数组,支持以下参数
include
或whitelist
- 数组或字符串;要包含在补丁中的绝对路径列表。exclude
或blacklist
- 数组或字符串;要排除在补丁中的绝对路径列表。cache-path
- 字符串;Pathchwork 应缓存封装文件的文件夹的绝对路径。redefinable-internals
- 数组;可以替换的内部 PHP 函数列表;如果内部函数(由 PHP 定义的函数)未在此列表中,则它永远不会在测试中被替换。
\tad\FunctionMocker\FunctionMocker::init([ 'whitelist' => [dirname(__DIR__) . '/src',dirname(__DIR__) . '/vendor'], 'blacklist' => [dirname(__DIR__) . '/included', dirname(__FILE__) . 'patchwork-cache', dirname(__DIR__)], 'cache-path' => dirname(__DIR__) . 'patchwork-cache, 'redefinable-internals' => ['time', 'filter_var'] ]);
排除项目根目录,例如示例中的 dirname(__DIR__)
,通常是一个好主意。
注意:即使没有为 FunctionMocker::init
方法提供配置,库也将忽略 patchwork.json
文件。
初始化参数
函数模拟器将负责使用一些合理的默认值初始化 Patchwork,但可以自定义这些初始化参数
whitelist
- 数组或字符串,默认为空;要包含在补丁中的绝对路径列表。blacklist
- 数组或字符串,默认为空;要排除在补丁中的绝对路径列表;Patchwork 库本身和 Function Mocker 库始终被排除。cache-path
- 字符串,默认为cache
文件夹;Pathcwork 应缓存封装文件的文件夹的绝对路径。redefinable-internals
数组,默认为空;可以替换的内部 PHP 函数列表;任何需要在测试中替换的 内部 函数(由 PHP 标准库定义)都应在此列出。
setUp 和 tearDown 方法
该库旨在用于PHPUnit测试用例的上下文中,并提供两个必须插入测试用例的setUp和tearDown方法中的static
方法,以确保功能模拟器能够正常工作。
class MyTest extends \PHPUnit_Framework_TestCase { public function setUp(){ // before any other set up method FunctionMocker::setUp(); ... } public function tearDown(){ ... // after any other tear down method FunctionMocker::tearDown(); } }
函数
替换函数
该库将允许在测试运行时使用FunctionMocker::replace
方法替换已定义和未定义的函数,如下所示:
FunctionMocker::replace('myFunction', $returnValue);
并且允许将返回值设置为实际值或函数回调
public function testReplacedFunctionReturnsValue(){ FunctionMocker::replace('myFunction', 23); $this->assertEquals(23, myFunction()); } public fuction testReplacedFunctionReturnsCallback(){ FunctionMocker::replace('myFunction', function($arg){ return $arg + 1; }); $this->assertEquals(24, myFunction()); }
如果您需要替换一个函数并使其返回一系列值,每次调用一个值,则可以使用FunctionMocker::replaceInOrder
方法
public function testReplacedFunctionReturnsValue(){ FunctionMocker::replaceInOrder('myFunction', [23, 89, 2389]); $this->assertEquals(23, myFunction()); $this->assertEquals(89, myFunction()); $this->assertEquals(2389, myFunction()); }
监视函数
如果FunctionMocker::replace
方法返回的值存储在一个变量中,那么可以对该函数进行调用检查
public function testReplacedFunctionReturnsValue(){ $myFunction = FunctionMocker::replace('myFunction', 23); $this->assertEquals(23, myFunction()); $myFunction->wasCalledOnce(); $myFunction->wasCalledWithOnce(23); }
可用于函数监视的方法在下面的“方法”部分中列出。
批量替换函数
当需要为组件工作提供一系列函数的占位符时,可以这样做
public function testBatchFunctionReplacement(){ $functions = ['functionOne', 'functionTwo', 'functionThree', ...]; FunctionMocker::replace($functions, function($arg){ return $arg; }); foreach ($functions as $f){ $this->assertEquals('foo', $f('foo')); } }
在替换一系列函数时,返回值将是一个可以按函数名称引用的间谍对象数组的array
public function testBatchFunctionReplacement(){ $functions = ['functionOne', 'functionTwo', 'functionThree', ...]; $replacedFunctions = FunctionMocker::replace($functions, function($arg){ return $arg; }); functionOne(); $functionOne = $replacedFunctions['functionOne']; $functionOne->wasCalledOnce(); }
静态方法
替换静态方法
与函数类似,该库将允许使用FunctionMocker::replace
方法替换已定义的静态方法
public function testReplacedStaticMethodReturnsValue(){ FunctionMocker::replace('Post::getContent', 'Lorem ipsum'); $this->assertEquals('Lorem ipsum', Post::getContent()); }
同样,可以设置回调函数的返回值
public function testReplacedStaticMethodReturnsCallback(){ FunctionMocker::replace('Post::formatTitle', function($string){ return "foo $string baz"; }); $this->assertEquals('foo lorem baz', Post::formatTitle('lorem')); }
请注意,只能替换
public static
方法。
监视静态方法
存储FunctionMocker::replace
函数的返回值允许使用下面的“方法”部分中列出的方法监视静态方法
public function testReplacedStaticMethodReturnsValue(){ $getContent = FunctionMocker::replace('Post::getContent', 'Lorem ipsum'); $this->assertEquals('Lorem ipsum', Post::getContent()); $getContent->wasCalledOnce(); $getContent->wasNotCalledWith('some'); ... }
批量替换静态方法
静态方法也可以批量替换,将相同的返回值或回调分配给任何替换的方法
public function testBatchReplaceStaticMethods(){ $methods = ['Foo::one', 'Foo::two', 'Foo::three']; FunctionMocker::replace($methods, 'foo'); $this->assertEquals('foo', Foo::one()); $this->assertEquals('foo', Foo::two()); $this->assertEquals('foo', Foo::three()); }
在批量替换静态方法时,FunctionMocker::replace
将返回一个按方法名称索引的间谍对象数组,可以用作任何其他静态方法间谍对象;
public function testBatchReplaceStaticMethods(){ $methods = ['Foo::one', 'Foo::two', 'Foo::three']; $replacedMethods = FunctionMocker::replace($methods, 'foo'); Foo::one(); $one = $replacedMethods['one']; $one->wasCalledOnce(); }
实例方法
替换实例方法
当尝试替换实例方法时,FunctionMocker::replace
方法将返回一个扩展的PHPUnit模拟对象,实现了所有原始方法和其中一些(见下文)
// file SomeClass.php class SomeClass{ protected $dep; public function __construct(Dep $dep){ $this->dep = $dep; } public function someMethod(){ return $this->dep->go(); } } // file SomeClassTest.php use tad\FunctionMocker\FunctionMocker; class SomeClassTest extends PHPUnit_Framework_TestCase { /** * @test */ public function it_will_call_go(){ $dep = FunctionMocker::replace('Dep::go', 23); $sut = new SomeClass($dep); $this->assertEquals(23, $sut->someMethod()); } }
FunctionMocker::replace
方法将使用any
方法设置PHPUnit模拟对象,上面的调用相当于
$dep->expects($this->any())->method('go')->willReturn(23);
如果需要在测试中替换多个实例方法,则存在这种实例方法替换的替代方法
use tad\FunctionMocker\FunctionMocker; class SomeClassTest extends \PHPUnit_Framework_TestCase { public function dependencyTest(){ $func = function($one, $two){ return $one + $two; }; $mock = FunctionMocker::replace('Dependency') ->method('methodOne') // replace with null returning methods ->method('methodTwo', 23) // replace the method and return a value ->method('methodThree', $func) ->get(); $this->assertNull($mock->methodOne()); $this->assertEquals(23, $mock->methodTwo()); $this->assertEquals(4, $mock->methodThree(1,3)); } }
如果不指定任何要替换的方法,将返回一个只替换了__construct
方法的模拟对象。
模拟链式方法
从版本0.2.13
开始,可以模拟旨在链式调用的实例方法。给定以下依赖类
class Query { ... public funtion where($column, $condition, $constraint){ ... return $this; } public function getResults(){ return $this->results; } ... }
和一个可能的客户端类
class QueryUser{ ... public function getOne($id){ $this->query ->where('ID', '=', $id) ->where('type', '=', $this->type) ->getFirst(); } ... }
在测试用例中使用->
作为返回值来模拟返回自身的where
方法
public function test_will_call_where_with_proper_args(){ // tell FunctionMocker to return the mock object itself when // the `where` method is called FunctionMocker::replace('Query::where', '->'); $query = FunctionMocker::replace('Query::getFirst', $mockResult); $sut = new QueryUser(); $sut->setQuery($query); // execute $sut->getOne(23); // verify ... }
模拟抽象类、接口和特性
FunctionMocker依赖于PHPUnit实例模拟引擎,保留了模拟接口、抽象类和特性的能力;用于此的目的的语法与模拟实例方法的语法相同
interface SalutingInterface { public function sayHi(); }
上面的接口可以像这样在测试中进行替换
public function test_say_hi(){ $mock = FunctionMocker::replace('SalutingInterface::sayHi', 'Hello World!'); // passes $this->assertEquals('Hello World!', $mock->sayHi()); }
有关更详细的方法,请参阅PHPUnit文档。
监视实例方法
调用实例方法时,FunctionMocker::replace
方法返回的对象允许使用“方法”部分中指定的方法来检查对替换方法的调用
// file SomeClass.php class SomeClass{ public function methodOne(){ ... } public function methodTwo(){ ... } } // file SomeClassTest.php use tad\FunctionMocker\FunctionMocker; class SomeClassTest extends PHPUnit_Framework_TestCase { /** * @test */ public function returns_the_same_replacement_object(){ // replace both class instance methods to return 23 $replacement = FunctionMocker::replace('SomeClass::methodOne', 23); // $replacement === $replacement2 $replacement2 = FunctionMocker::replace('SomeClass::methodTwo', 23); $replacement->methodOne(); $replacement->methodTwo(); $replacement->wasCalledOnce('methodOne'); $replacement->wasCalledOnce('methodTwo'); } }
另一个更流畅的API允许以上断言以更类似于prophecy使用的这种方式进行重写
// file SomeClassTest.php use tad\FunctionMocker\FunctionMocker; class SomeClassTest extends PHPUnit_Framework_TestCase { /** * @test */ public function returns_the_same_replacement_object(){ // replace both class instance methods to return 23 $mock = FunctionMocker::replace('SomeClass) ->methodOne() ->methodTwo(); $replacement = $mock->get(); // think of $mock->reveal() $replacement->methodOne(); $replacement->methodTwo(); $mock->verify()->methodOne()->wasCalledOnce(); $mock->verify()->methodTwo()->wasCalledOnce(); } }
批量替换实例方法
可以使用与批量函数和静态方法替换相同的语法进行批量实例替换。
给定上面的SomeClass
public function testBatchInstanceMethodReplacement(){ $methods = ['SomeClass::methodOne', 'SomeClass::methodTwo']; // replace both class instance methods to return 23 $replacements = FunctionMocker::replace($methods, 23); $replacement[0]->methodOne(); $replacement[1]->methodTwo(); $replacement[0]->wasCalledOnce('methodOne'); $replacement[1]->wasCalledOnce('methodTwo'); }
方法
除了作为PHPUnit模拟对象接口一部分定义的方法(参见此处),这些方法仅在替换实例方法时可用,函数模拟器会扩展替换的函数和方法以下方法
wasCalledTimes(int $times [, string $methodName])
- 如果函数或静态方法被调用$times
次,则将断言PHPUnit断言;$times
参数可以使用以下时间语法使用。wasCalledOnce([string $methodName])
- 如果函数或静态方法被调用一次,则将断言PHPUnit断言。wasNotCalled([string $methodName])
- 如果函数或静态方法没有被调用,则将断言PHPUnit断言。wasCalledWithTimes(array $args, int $times[, string $methodName])
- 如果函数或静态方法被带有$args
参数调用$times
次,则将断言PHPUnit断言;$times
参数可以使用以下时间语法使用;$args
参数可以是原始值和PHPUnit约束(如[23, Test::isInstanceOf('SomeClass')]
)的任意组合。wasCalledWithOnce(array $args[, string $methodName])
- 如果函数或静态方法被带有$args
参数调用一次,则将断言PHPUnit断言;$args
参数可以是原始值和PHPUnit约束(如[23, Test::isInstanceOf('SomeClass')]
)的任意组合。wasNotCalledWith(array $args[, string $methodName])
- 如果函数或静态方法没有被带有$args
参数调用,则将断言PHPUnit断言;$args
参数可以是原始值和PHPUnit约束(如[23, Test::isInstanceOf('SomeClass')]
)的任意组合。
需要方法名来验证替换的实例方法的调用!
时间
指定函数或方法应该被调用的次数时,可以使用灵活的语法;在最基本的形式中,可以用数字表示
// the function should have have been called exactly 2 times $function->wasCalledTimes(2);
但使用字符串可以使检查更简单,可以使用PHP中使用的比较器语法
// the function should have been called at least 2 times $function->wasCalledTimes('>=2');
可用的比较器有 >n
、<n
、>=n
、<=n
、==n
(与插入数字相同)、!n
。
糖方法
函数模拟器包含一些糖方法,以使我的测试生活更加轻松。任何这些方法的结果都可以使用替代代码实现,但我已实现这些方法以加快速度。
测试方法
函数模拟器包装一个PHPUnit_Framework_TestCase
,允许以静态方式调用通常在$this
上调用的测试方法。一个测试方法可以写成这样
use tad\FunctionMocker\FunctionMocker as Test; class SomeTest extends \PHPUnit_Framework_TestCase { public function test_true() { $this->assertTrue(true); } public function test_wrapped_true_work_the_same() { Test::assertTrue(true); } }
作为一个简单的包装,要使用的测试用例可以通过测试用例中的setUp
方法中的setTestCase
静态方法设置
public function setUp() { FunctionMocker::setTestCase($this); }
并且任何特定于测试用例的方法都将作为tad\FunctionMocker\FunctionMocker
类的静态方法可用。
除了包装测试用例定义的方法之外,任何由PHPUnit_Framework_TestCase
类定义的方法都可用于自动完成,以便于阅读IDE,如PhpStorm或Sublime Text。
替换全局变量
允许在测试后用模拟对象替换全局变量并恢复它。最好用于替换/设置全局对象实例以模拟;例如
FunctionMocker::replaceGlobal('wpdb', 'wpdb::get_row', $rowData); // this will access $wpdb->get_row() $post = get_latest_post(); // verify $this->assertEquals(...);
等同于编写
// prepare $mockWpdb = FunctionMocker::replace('wpdb::get_row', $rowData); $prevWpdb = isset($GLOBALS['wpdb']) ? $GLOBALS['wpdb'] : null; $GLOBALS['wpdb'] = $mockWpdb; // this will access $wpdb->get_row() $post = get_latest_post(); // verify $this->assertEquals(...); // restore state $GLOBALS['wpdb'] = $prevWpdb;
设置全局变量
允许替换/设置全局值并在测试后恢复其状态。
FunctionMocker::setGlobal('switchingToTheme', 'foo'); $do_action = FunctionMocker::replace('do_action'); // exercitate call_switch_theme_actions(); $do_action->wasCalledWithOnce(['before_switch_to_theme_foo', ])
等同于编写
$prev = isset($GLOBALS['switchingToTheme']) ? $GLOBALS['switchingToTheme'] : null; $GLOBALS['switchingToTheme'] = 'foo'; $do_action = FunctionMocker::replace('do_action'); // exercitate call_switch_theme_actions(); // verify $do_action->wasCalledWithOnce(['before_switch_to_theme_foo', ]) // restore state $GLOBALS['switchingToTheme'] = $prev;