仿件对象(Mock Object)
最后更新于:2022-04-01 03:45:51
# 仿件对象(Mock Object)
将对象替换为能验证预期行为(例如断言某个方法必会被调用)的测试替身的实践方法称为*模仿(mocking)*。
可以用 *仿件对象(mock object)*“作为观察点来核实被测试系统在测试中的间接输出。通常,仿件对象还需要包括桩件的功能,因为如果测试尚未失败则仿件对象需要向被测系统返回一些值,但是其重点还是在对间接输出的核实上。因此,仿件对象远不止是桩件加断言,它是以一种从根本上完全不同的方式来使用的”(Gerard Meszaros)。
### 局限性:对预期的自动校验
PHPUnit只会对在某个测试的作用域内生成的仿件对象进行自动校验。诸如在数据供给器内生成或用`@depends` 标注注入测试的仿件对象,PHPUnit并不会自动对其进行校验。
这有个例子:假设需要测试的当前方法,在例子中是 `update()`,确实在一个观察着另外一个对象的对象中上被调用了。[Example 9.10, “被测系统(SUT)中 Subject 与 Observer 类的代码”](# "Example 9.10. 被测系统(SUT)中 Subject 与 Observer 类的代码")展示了被测系统(SUT)中 `Subject` 和 `Observer` 两个类的代码。
**Example 9.10. 被测系统(SUT)中 Subject 与 Observer 类的代码**
~~~
<?php
class Subject
{
protected $observers = array();
protected $name;
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
public function attach(Observer $observer)
{
$this->observers[] = $observer;
}
public function doSomething()
{
// 做点什么。
// ...
// 通知观察者。
$this->notify('something');
}
public function doSomethingBad()
{
foreach ($this->observers as $observer) {
$observer->reportError(42, 'Something bad happened', $this);
}
}
protected function notify($argument)
{
foreach ($this->observers as $observer) {
$observer->update($argument);
}
}
// 其他方法。
}
class Observer
{
public function update($argument)
{
// 做点什么。
}
public function reportError($errorCode, $errorMessage, Subject $subject)
{
// 做点什么。
}
// 其他方法。
}
?>
~~~
[Example 9.11, “测试某个方法会以特定参数被调用一次”](# "Example 9.11. 测试某个方法会以特定参数被调用一次")展示了如何用仿件对象来测试 `Subject` 和 `Observer` 对象之间的互动。
首先用 `PHPUnit_Framework_TestCase` 类提供的 `getMock()` 方法建立 `Observer` 的仿件对象。由于给出了一个数组做为 `getMock()` 方法的第二(可选)参数,`Observer` 类只有 `update()` 方法会被替换为仿实现。
由于关注的是检验某个方法是否被调用,以及调用时具体所使用的参数,因此引入 `expects()` 与 `with()` 方法来指明此交互应该是什么样的。
**Example 9.11. 测试某个方法会以特定参数被调用一次**
~~~
<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
public function testObserversAreUpdated()
{
// 为 Observer 类建立仿件对象,只模仿 update() 方法。
$observer = $this->getMockBuilder('Observer')
->setMethods(array('update'))
->getMock();
// 建立预期状况:update() 方法将会被调用一次,
// 并且将以字符串 'something' 为参数。
$observer->expects($this->once())
->method('update')
->with($this->equalTo('something'));
// 创建 Subject 对象,并将模仿的 Observer 对象连接其上。
$subject = new Subject('My subject');
$subject->attach($observer);
// 在 $subject 对象上调用 doSomething() 方法,
// 预期将以字符串 'something' 为参数调用
// Observer 仿件对象的 update() 方法。
$subject->doSomething();
}
}
?>
~~~
`with()` 方法可以携带任何数量的参数,对应于被模仿的方法的参数数量。可以对方法的参数指定更加高等的约束而不仅是简单的匹配。
**Example 9.12. 测试某个方法将会以特定数量的参数进行调用,并且对各个参数以多种方式进行约束**
~~~
<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
public function testErrorReported()
{
// 为 Observer 类建立仿件,对 reportError() 方法进行模仿
$observer = $this->getMockBuilder('Observer')
->setMethods(array('reportError'))
->getMock();
$observer->expects($this->once())
->method('reportError')
->with(
$this->greaterThan(0),
$this->stringContains('Something'),
$this->anything()
);
$subject = new Subject('My subject');
$subject->attach($observer);
// doSomethingBad() 方法应当会通过(observer的)reportError()方法
//向 observer 报告错误。
$subject->doSomethingBad();
}
}
?>
~~~
`withConsecutive()` 方法可以接受任意多个数组作为参数,具体数量取决于欲测试的调用。每个数组都都是对被仿方法的相应参数的一组约束,就像 `with()` 中那样。
**Example 9.13. 测试某个方法将会以特定参数被调用二次**
~~~
<?php
class FooTest extends PHPUnit_Framework_TestCase
{
public function testFunctionCalledTwoTimesWithSpecificArguments()
{
$mock = $this->getMockBuilder('stdClass')
->setMethods(array('set'))
->getMock();
$mock->expects($this->exactly(2))
->method('set')
->withConsecutive(
array($this->equalTo('foo'), $this->greaterThan(0)),
array($this->equalTo('bar'), $this->greaterThan(0))
);
$mock->set('foo', 21);
$mock->set('bar', 48);
}
}
?>
~~~
`callback()` 约束用来进行更加复杂的参数校验。此约束的唯一参数是一个 PHP 回调项(callback)。此 PHP 回调项接受需要校验的参数作为其唯一参数,并应当在参数通过校验时返回 `TRUE`,否则返回 `FALSE`。
**Example 9.14. 更加复杂的参数校验**
~~~
<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
public function testErrorReported()
{
// 为 Observer 类建立仿件,模仿 reportError() 方法
$observer = $this->getMockBuilder('Observer')
->setMethods(array('reportError'))
->getMock();
$observer->expects($this->once())
->method('reportError')
->with($this->greaterThan(0),
$this->stringContains('Something'),
$this->callback(function($subject){
return is_callable(array($subject, 'getName')) &&
$subject->getName() == 'My subject';
}));
$subject = new Subject('My subject');
$subject->attach($observer);
// doSomethingBad() 方法应当会通过(observer的)reportError()方法
//向 observer 报告错误。
$subject->doSomethingBad();
}
}
?>
~~~
**Example 9.15. 测试某个方法将会被调用一次,并且以某个特定对象作为参数。**
~~~
<?php
class FooTest extends PHPUnit_Framework_TestCase
{
public function testIdenticalObjectPassed()
{
$expectedObject = new stdClass;
$mock = $this->getMockBuilder('stdClass')
->setMethods(array('foo'))
->getMock();
$mock->expects($this->once())
->method('foo')
->with($this->identicalTo($expectedObject));
$mock->foo($expectedObject);
}
}
?>
~~~
**Example 9.16. 创建仿件对象时启用参数克隆**
~~~
<?php
class FooTest extends PHPUnit_Framework_TestCase
{
public function testIdenticalObjectPassed()
{
$cloneArguments = true;
$mock = $this->getMockBuilder('stdClass')
->enableArgumentCloning()
->getMock();
// 现在仿件将对参数进行克隆,因此 identicalTo 约束将会失败。
}
}
?>
~~~
[Table A.1, “约束条件”](# "Table A.1. 约束条件")列出了可以应用于方法参数的各种约束,[Table 9.1, “匹配器”](# "Table 9.1. 匹配器")列出了可以用于指定调用次数的各种匹配器。
**Table 9.1. 匹配器**
| 匹配器 | 含义 |
|-----|-----|
| PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount any() | 返回一个匹配器,当被评定的方法执行0次或更多次(即任意次数)时匹配成功。 |
| PHPUnit_Framework_MockObject_Matcher_InvokedCount never() | 返回一个匹配器,当被评定的方法从未执行时匹配成功。 |
| PHPUnit_Framework_MockObject_Matcher_InvokedAtLeastOnce atLeastOnce()` | 返回一个匹配器,当被评定的方法执行至少一次时匹配成功。 |
| PHPUnit_Framework_MockObject_Matcher_InvokedCount once() | 返回一个匹配器,当被评定的方法执行恰好一次时匹配成功。 |
| PHPUnit_Framework_MockObject_Matcher_InvokedCount exactly(int $count) | 返回一个匹配器,当被评定的方法执行恰好 `$count` 次时匹配成功。 |
| PHPUnit_Framework_MockObject_Matcher_InvokedAtIndex at(int $index) | 返回一个匹配器,当被评定的方法是第 `$index` 个执行的方法时匹配成功。 |
>[info] ### Note
> `at()` 匹配器的 `$index` 参数指的是对给定仿件对象的*所有方法的调用*的索引,从零开始。使用这个匹配器要谨慎,因为它可能导致测试由于与具体的实现细节过分紧密绑定而变得脆弱。