间谍(Spies)
间谍 是一种测试替身,但它们与存根或模拟对象的不同之处在于,间谍会记录间谍与被测系统(SUT)之间的任何交互,并允许我们在事后对这些交互进行断言。
创建一个间谍意味着我们不必为测试期间替身可能接收到的每个方法调用设置期望,其中一些可能与当前测试无关。间谍允许我们只对本次测试关心的调用进行断言,从而减少过度指定的可能性,并使我们的测试更加清晰。
间谍还允许我们在测试中遵循更熟悉的 “安排-行动-断言”(Arrange-Act-Assert) 或 “给定-当-则”(Given-When-Then) 风格。使用模拟对象,我们必须遵循一种不太熟悉的风格,类似于 “安排-期望-行动-断言”(Arrange-Expect-Act-Assert),在这种风格中,我们必须在对被测系统采取行动之前告诉模拟对象期望什么,然后断言这些期望得到了满足:
// 安排
$mock = \Mockery::mock('MyDependency');
$sut = new MyClass($mock);
// 期望
$mock->shouldReceive('foo')
->once()
->with('bar');
// 行动
$sut->callFoo();
// 断言
\Mockery::close();
间谍允许我们跳过 “期望” 部分,并在对被测系统采取行动之后移动断言,这通常会使我们的测试更具可读性:
// 安排
$spy = \Mockery::spy('MyDependency');
$sut = new MyClass($spy);
// 行动
$sut->callFoo();
// 断言
$spy->shouldHaveReceived()
->foo()
->with('bar');
另一方面,间谍远没有模拟对象那么严格,这意味着测试通常不那么精确,因为它们让我们忽略了更多。这通常是件好事,它们应该只在需要时才精确,但虽然间谍使我们的测试更能揭示意图,但它们确实倾向于较少揭示被测系统的设计。如果我们必须在许多不同的测试中为模拟对象设置大量期望,我们的测试正在试图告诉我们一些事情——被测系统做了太多事情,可能应该重构。使用间谍,我们得不到这种提示,它们只是忽略了与它们不相关的调用。
使用间谍的另一个缺点是调试。当一个模拟对象接收到它没有预料到的调用时,它会立即抛出异常(快速失败),给我们一个很好的堆栈跟踪,甚至可能调用我们的调试器。使用间谍,我们只是在事后断言调用是否发生,所以如果发生了错误的调用,我们没有像使用模拟对象时那样的“及时”上下文。
最后,如果我们需要为测试替身定义返回值,我们不能使用间谍来做到这一点,只能使用模拟对象。
间谍参考
要验证某个方法是否在间谍上被调用,我们使用 shouldHaveReceived() 方法:
$spy->shouldHaveReceived('foo');
要验证某个方法 未 在间谍上被调用,我们使用 shouldNotHaveReceived() 方法:
$spy->shouldNotHaveReceived('foo');
我们还可以使用间谍进行参数匹配:
$spy->shouldHaveReceived('foo')
->with('bar');
通过传入要匹配的参数数组,也可以进行参数匹配:
$spy->shouldHaveReceived('foo', ['bar']);
然而,在验证某个方法未被调用时,参数匹配只能通过将参数数组作为 shouldNotHaveReceived() 方法的第二个参数来提供:
$spy->shouldNotHaveReceived('foo', ['bar']);
这是由于 Mockery 的内部机制所致。
最后,在期望应该被接收的调用时,我们还可以验证调用次数:
$spy->shouldHaveReceived('foo')
->with('bar')
->twice();
替代的 shouldReceive 语法
从 Mockery 1.0.0 开始,我们支持像调用任何 PHP 方法一样调用方法,而不是将方法名作为字符串参数传递给 Mockery 的 should* 方法。
对于间谍,这仅适用于 shouldHaveReceived() 方法:
$spy->shouldHaveReceived()
->foo('bar');
我们也可以设置调用次数的期望:
$spy->shouldHaveReceived()
->foo('bar')
->twice();
不幸的是,由于限制,我们无法为 shouldNotHaveReceived() 方法支持相同的语法。