sirbrillig / spies
PHP 测试中的简化间谍、存根和模拟
Requires
- php: >=5.3.0
Requires (Dev)
- antecedent/patchwork: ^2.0.0
- phpunit/phpunit: ^5.2
This package is auto-updated.
Last update: 2024-09-11 04:53:53 UTC
README
这是一个使 PHP 测试变得更加简单的库。您可以通过以下说明将其安装到 PHP 项目中。
它是什么?如果您曾经使用过 JavaScript 测试中的 sinon,您就知道 测试间谍 的概念,并且这个库在许多方面只是将这些概念应用于 PHP。它还包括 期望,以简化间谍断言,灵感来自 sinon-chai。
如果您想直接查看详细信息,可以 在这里阅读 API。
如果您不熟悉测试间谍,这里有一个简要的介绍:基本上,它们是表现像函数一样并记录它们如何被调用的对象。您可以在编写测试时将它们注入到对象中,并监视间谍以确定对象是否以您期望的方式表现。
$spy = new \Spies\Spy(); $spy( 'hello', 'world' ); $spy->was_called(); // Returns true $spy->was_called_times( 1 ); // Returns true $spy->was_called_times( 2 ); // Returns false $spy->get_times_called(); // Returns 1 $spy->was_called_with( 'hello', 'world' ); // Returns true $spy->was_called_with( 'goodbye', 'world' ); // Returns false
间谍也可以编程以以某种方式表现(在这种情况下,它们更正确地称为“存根”或“模拟”),强制您的代码沿着特定的路径执行,以测试特定的行为。
\Spies\stub_function( 'add_one' )->when_called->with( 5 )->will_return( 6 ); \Spies\stub_function( 'add_one' )->when_called->with( 1 )->will_return( 2 ); add_one( 5 ); // Returns 6 add_one( 1 ); // Returns 2
在 PHP 中,我们经常需要监视具有实例方法的整个对象,因此间谍提供了一种机制来实现这一点。
class Greeter { public function say_hello() { return 'hello'; } public function say_goodbye() { return 'goodbye'; } } function test_greeter() { $mock = \Spies\mock_object_of( 'Greeter' ); $mock->add_method( 'say_hello' )->that_returns( 'greetings' ); $greet = $mock->spy_on_method( 'greet' ); $this->assertEquals( 'greetings', $mock->say_hello() ); $this->assertEquals( null, $mock->say_goodbye() ); $mock->greet(); $this->assertTrue( $greet->was_called() ); }
最后一部分,期望为测试断言添加了一层语法,这使得测试断言更容易阅读,并提供更好的失败信息。
function test_spy_is_called_correctly() { $spy = \Spies\make_spy(); $spy( 'hello', 'world', 7 ); $spy( 'hello', 'world', 8 ); $expectation = \Spies\expect_spy( $spy )->to_have_been_called->with( 'hello', 'world', \Spies\any() )->twice(); $expectation->verify(); }
间谍被设计为优秀的 WP_Mock 和 Mockery 的可选替代品,这两个都是非常强大的库,但有很多方面和怪癖,我认为它们不太直观。
欢迎提出建议、错误报告和功能请求!
安装
这是一个使 PHP 测试变得更加简单的库。您可以通过运行以下命令将其安装到 PHP 项目中:
composer require --dev sirbrillig/spies
.
然后确保您在代码的某个位置包含了自动加载器
如果使用 PHPUnit,您可以将自动加载添加到您的 phpunit.xml
文件中
<phpunit bootstrap="vendor/autoload.php">
...
否则您可以手动包含自动加载器
require( './vendor/autoload.php' );
但是,请参阅 关于模拟和监视现有函数的说明。如果您需要这样做,您需要添加一个显式的引导文件。
详细信息
全局函数
如果您想为一个全局函数(如 WordPress 的 wp_insert_post
)创建间谍,您可以将全局函数的名称传递给 \Spies\get_spy_for()
function test_calculation() { $add_one = \Spies\get_spy_for( 'add_together' ); add_together( 2, 3 ); $expectation = \Spies\expect_spy( $add_one )->to_have_been_called->with( 2, 3 ); // Passes $expectation->verify(); }
您也可以以相同的方式在同一个命名空间中定义的函数上监视
function test_calculation() { $add_one = \Spies\get_spy_for( '\Calculator\add_together' ); \Calculator\add_together( 2, 3 ); $expectation = \Spies\expect_spy( $add_one )->to_have_been_called->with( 2, 3 ); // Passes $expectation->verify(); }
存根和模拟
您可以使用 \Spies\stub_function()
方法创建存根。存根是一个可以像真实函数一样调用的虚构函数,除了您可以控制其行为。
存根也可以像间谍一样模拟全局函数或命名空间函数。实际上,存根也是一个间谍,这意味着您可以查询您想要的任何信息。
您可以在存根中编程一些基本行为
- 您可以使用一个来替换全局函数(它将返回 null)。
- 您可以使用一个在调用时返回特定值。
- 您可以使用一个函数在传递特定参数时返回一个特定值。
- 您可以使用一个函数返回它所接收到的其中一个参数。
- 您可以使用一个函数调用替代函数。
这里只是设置一个返回值
\Spies\stub_function( 'get_color' )->and_return( 'green' ); get_color(); // Returns 'green'
这里使用某些参数返回一个值
\Spies\stub_function( 'add_one' )->when_called->with( 5 )->will_return( 6 ); \Spies\stub_function( 'add_one' )->when_called->with( 1 )->will_return( 2 ); add_one( 5 ); // Returns 6 add_one( 1 ); // Returns 2
这里返回一个参数
\Spies\stub_function( 'get_first' )->when_called->will_return( \Spies\passed_arg( 0 ) ); get_first( 5, 6, 7 ); // Returns 5 get_first( 1, 2, 3 ); // Returns 1
这里返回替代函数的结果
\Spies\stub_function( 'add_one' )->and_return( function( $a ) { return $a + 1; } ); add_one( 5 ); // Returns 6 add_one( 1 ); // Returns 2
对象
有时您需要创建一个具有函数占位符的整个对象。在这种情况下,您可以使用 \Spies\mock_object()
,这将返回一个可以被传递的对象。默认情况下,该对象没有方法,但您可以使用 add_method()
来添加一些。
add_method()
,或其别名 spy_on_method()
,在没有第二个参数的情况下被调用时,返回一个占位符(记住,占位符也是一个Spy),因此您可以编程其行为或查询其期望。您也可以使用第二个参数显式地传递一个函数(或Spy),在这种情况下,您传递的任何内容都将被返回。
function test_calculation() { $adder = \Spies\mock_object(); $adder->add_method( 'add_one' )->when_called->with( 6 )->will_return( 7 ); $add_one = $adder->spy_on_method( 'add_one' ); $calculator = new Calculator( $adder ); $calculator->add_one( 4 ); // Returns null $calculator->add_one( 6 ); // Returns 7 \Spies\expect_spy( $add_one )->to_have_been_called(); // Passes \Spies\expect_spy( $add_one )->to_have_been_called->with( 2 ); // Fails \Spies\finish_spying(); // Verifies all Expectations }
对于您尝试模拟的现有类的每个公共方法,调用 add_method()
可能会很麻烦。因此,您可以使用 \Spies\mock_object_of( $class_name )
,这将创建一个 MockObject
并自动为原始类上的每个公共方法添加一个Spy。这些Spy默认返回null,但您可以使用与上面相同的 add_method()
替换其中的任何一个。
class Greeter { public function say_hello() { return 'hello'; } public function say_goodbye() { return 'goodbye'; } } function test_greeter() { $mock = \Spies\mock_object_of( 'Greeter' ); $mock->add_method( 'say_hello' )->that_returns( 'greetings' ); $this->assertEquals( 'greetings', $mock->say_hello() ); $this->assertEquals( null, $mock->say_goodbye() ); }
如果您不想调用 add_method()
且没有原始类来复制,您也可以使用 and_ignore_missing()
忽略对象上的所有方法调用。
function test_greeter() { $mock = \Spies\mock_object()->and_ignore_missing(); $this->assertEquals( null, $mock->say_goodbye() ); }
对象方法代理
有时能够监视对象的实际方法,或者替换对象上的某些方法,但不是所有方法,是有帮助的。这涉及到创建一个代理对象,可以通过将类实例传递给 \Spies\mock_object()
来完成。
结果 MockObject
将将所有方法调用转发到原始类实例,除了使用 add_method()
覆盖的方法。您可以使用 spy_on_method()
监视对象的任何方法调用,就像您会使用常规MockObject一样。
class Greeter { public function say_hello() { return 'hello'; } public function say_goodbye() { return 'goodbye'; } } function test_greeter() { $mock = \Spies\mock_object( new Greeter() ); $say_goodbye = $mock->spy_on_method( 'say_goodbye' ); $mock->add_method( 'say_hello' )->that_returns( 'greetings' ); $this->assertEquals( 'greetings', $mock->say_hello() ); $this->assertEquals( 'goodbye', $mock->say_goodbye() ); $this->assertSpyWasCalled( $say_goodbye ); }
期望
Spy本身很有用,但Spy还提供了 Expectation
类,以便更容易编写测试期望。
假设我们有一个Spy,并想在PHPUnit测试中验证它是否被调用
function test_spy_is_called() { $spy = \Spies\make_spy(); $spy(); $this->assertTrue( $spy->was_called() ); }
这可行,但这里还有一种写法
function test_spy_is_called() { $spy = \Spies\make_spy(); $spy(); $expectation = \Spies\expect_spy( $spy )->to_have_been_called(); $expectation->verify(); }
它们都是完全有效的。期望只是为您的测试添加了一些语法糖,并通过改进错误消息来加速调试过程。特别是,它们允许构建一组期望的行为,然后一次性验证所有这些行为。让我们用一个更复杂的例子来说明。这是只有一个Spy的情况
function test_spy_is_called_correctly() { $spy = \Spies\make_spy(); $spy( 'hello', 'world', 7 ); $spy( 'hello', 'world', 8 ); $this->assertTrue( $spy->was_called_with( 'hello', 'world', \Spies\any() ) ); $this->assertTrue( $spy->was_called_times( 2 ) ); }
这是使用期望的情况
function test_spy_is_called_correctly() { $spy = \Spies\make_spy(); $spy( 'hello', 'world', 7 ); $spy( 'hello', 'world', 8 ); $expectation = \Spies\expect_spy( $spy )->to_have_been_called->with( 'hello', 'world', \Spies\any() )->twice(); $expectation->verify(); }
最后一部分,$expectation->verify()
实际上测试所有期望的行为。您还可以调用函数 \Spies\finish_spying()
做同样的事情,并且可以将其放在 tearDown
方法中。
更好的失败信息
期望最有用的地方可能是它们提供了更好的失败信息。与 $this->assertTrue( $spy->was_called_with( 'hello' ) )
和 \Spies\expect_spy( $spy )->to_have_been_called->with( 'hello' )
都断言相同的事情,但前者只会告诉你“false不是true”,而期望将失败并带有类似以下的消息
Expected "anonymous function" to be called with ['hello'] but instead it was called with ['goodbye']
finish_spying
为了在测试期间完成期望,并保持全局作用域中的函数不会相互干扰,在每次测试后调用 \Spies\finish_spying()
是 非常重要的。
finish_spying()
做了三件事
- 对每个期望调用
verify()
。expect_spy()
只准备期望。它只有在调用verify()
时才会被测试。 - 清除所有当前的 Spy 和模拟函数。
- 清除所有当前的期望。
因为期望只有在调用 verify()
或 finish_spying()
时才会被评估,所以你可以在测试代码之前或之后使用期望。以下两种方式是相同的
function tearDown() { \Spies\finish_spying(); } function test_calculation() { $add_one = \Spies\get_spy_for( 'add_together' ); add_together( 2, 3 ); \Spies\expect_spy( $add_one )->to_have_been_called->with( 2, 3 ); // Passes }
function tearDown() { \Spies\finish_spying(); } function test_calculation() { $add_one = \Spies\get_spy_for( 'add_together' ); \Spies\expect_spy( $add_one )->to_be_called->with( 2, 3 ); // Passes add_together( 2, 3 ); }
参数列表
如果你使用 with()
测试期望,有时你不在乎参数的值。在这种情况下,你可以用 \Spies\Expectation::any()
代替该参数
function tearDown() { \Spies\finish_spying(); } function test_calculation() { $add_one = \Spies\get_spy_for( 'add_together' ); \Spies\expect_spy( $add_one )->to_be_called->with( \Spies\Expectation::any(), \Spies\Expectation::any() ); // Passes add_together( 2, 3 ); }
如果你需要匹配字符串的一部分,可以使用 \Spies\match_pattern()
。
$spy = \Spies\get_spy_for( 'run_experiment' ); run_experiment( 'slartibartfast' ); \Spies\expect_spy( $spy )->to_have_been_called->with( \Spies\match_pattern( '/bart/' ) ); \Spies\finish_spying();
你还可以使用 \Spies\match_array()
来匹配数组中的元素,同时忽略其他部分
function tearDown() { \Spies\finish_spying(); } function test_name() { $say_hello = \Spies\get_spy_for( 'say_hello' ); \Spies\expect_spy( $say_hello )->to_be_called->with( \Spies\match_array( [ 'name' => 'Raistlin' ] ) ) ); // Passes say_hello( [ 'name' => 'Raistlin', 'job' => 'wizard', 'robes' => 'black' ] ); }
PHPUnit 自定义断言
如果你更喜欢使用 PHPUnit 自定义断言而不是期望,这些断言也是可用的(虽然你必须基于 \Spies\TestCase
创建测试类)
class MyTest extends \Spies\TestCase { function test_spy_is_called_correctly() { $spy = \Spies\make_spy(); $spy( 'hello', 'world', 7 ); $spy( 'hello', 'world', 8 ); $this->assertSpyWasCalledWith( $spy, [ 'hello', 'world', \Spies\any() ] ); } }
自定义断言将提供有关测试失败原因的详细信息,这比“false 不是 true”要好得多。
Failed asserting that a spy is called with arguments: ( "a", "b", "c" ).
a spy was actually called with:
1. arguments: ( "b", "b", "c" ),
2. arguments: ( "m", "b", "c" )
有关可用的自定义断言的完整列表,请参阅API 文档。
断言助手
对于任何断言,即使不涉及 Spy 或 Stub,比较与 match_array()
相同的数组部分也是有帮助的。你可以使用辅助函数 do_arrays_match()
来完成此操作
$array = [ 'baz' => 'boo', 'foo' => 'bar' ]; $this->assertTrue( \Spies\do_arrays_match( $array, \Spies\match_array( [ 'foo' => 'bar' ] ) ) );
监视和模拟现有函数
PHP 不允许模拟现有函数。然而,有一个名为 Patchwork 的库允许这样做。如果加载了该库,它将由 Spies 使用。该库必须在 Spies 之前加载。一种方法是使用测试引导文件。
如果你在 PHPUnit 中使用,你可以从 phpunit.xml
文件中包含引导文件
<phpunit bootstrap="tests/bootstrap.php">
...
以下是一个示例引导文件,它还加载了自动加载器
<?php $autoload = 'vendor/autoload.php'; $patchwork = 'vendor/antecedent/patchwork/Patchwork.php'; # require patchwork first if ( file_exists( $patchwork ) ) { require_once $patchwork; } if ( file_exists( $autoload ) ) { require_once $autoload; }
如果加载了 Patchwork,你将能够对现有函数使用 mock_function()
和 get_spy_for()
function sayHello() { return 'hello'; } //... \Spies\mock_function( 'sayHello' )->and_return( 'bye' ); $this->assertEquals( 'bye', sayHello() );
function sayHello() { return 'hello'; } //... $spy = \Spies\get_spy_for( 'sayHello' ); sayHello(); $this->assertTrue( $spy->was_called() );
贡献
请提交问题或 PR!