迷惘的码农。
分类:
2008-04-01 18:12:16
有时你需要检查某对象是否被正确调用了。看个例子:假如我们要测试是否正确调用了某观测另一对象的对象的方法,本例中是update()
。
在中,我们先用类PHPUnit_Framework_TestCase
(见)提供的getMock()
方法为Observer
装配一个模拟对象。我们给出一个数组作为getMock()
方法的第二个参数(可选的),因此只有类Observer
的update()
方法被替换为模拟实现。
然后我们使用PHPUnit提供的指定仿制品的行为和期望值。大体上,这意味着你不需要创建若干临时对象——例如,一个用于指定你期望update()
方法被调用,另一个用于期望的参数——然后把它们绑在一起配置期望值。 如例中所示,改为链接方法调用。这使得代码更易读和“流畅”。
范例 11.1: 测试某方法被附以指定的参数调用一次
require_once 'PHPUnit/Framework.php';
class ObserverTest extends PHPUnit_Framework_TestCase
{
public function testUpdateIsCalledOnce()
{
// 为Observer类创建一个只模拟update()方法的模拟对象。
$observer = $this->getMock('Observer', array('update'));
// 设定update()方法的期望值为只被以字符串“something”为参数调用一次。
$observer->expects($this->once())
->method('update')
->with($this->equalTo('something'));
// 创建一个Subject对象并附上模拟的Observer对象。
$subject = new Subject;
$subject->attach($observer);
// 调用$subject对象的doSomething()方法,我们期望该方法
// 以字符串“something”为参数调用模拟的Observer对象的update()方法。
$subject->doSomething();
}
}
?>
列出可用的匹配器,它们可用于表示期望模拟的方法要被运行多少次。
表 11.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 上被调用时匹配。 |
在中可以找到一些约束条件,它们可被一起用于这些匹配器。
另外,你能应用自分流模式代替模拟对象来测试Subject
实现。该方法将测试用例自身用作存根。术语自分流来自医学行业的实践,安装管子从动脉取血并放回静脉来为注射麻药提供合适的位置。
首先,我们让测试用例类实现Observer
,它是要观测Subject
的对象要实现的接口:
class ObserverTest extends PHPUnit_Framework_TestCase implements Observer
{
}
下一步,我们实现一个Observer
方法,update()
,来检查当被观测的Subject
对象的状态改变时该方法被调用:
public $wasCalled = FALSE;
public function update(Subject $subject)
{
$this->wasCalled = TRUE;
}
现在我们可以写测试了。我们创建一个新的Subject
对象并附上测试对象作为观测者。当Subject
的状态改变——例如,通过调用它的doSomething()
方法——Subject
对象必须调用注册为观测者的所有对象的update()
方法。我们用我们的update()
实现中设置的$wasCalled
实例变量检查Subject
对象是否做期望的事情:
public function testUpdate()
{
$subject = new Subject;
$subject->attach($this);
$subject->doSomething();
$this->assertTrue($this->wasCalled);
}
注意我们创建一个新的Subject
对象而不是依赖一个全局实例。存根鼓励这种形式的设计。它降低对象间的耦合并改善重用性。
如果你不熟悉自分流模式,(会感觉)测试很难阅读。这是什么?为什么一个测试用例同时也是个观测者?一旦你习惯这种用语,测试就易于阅读了。要了解的是测试的所有东西在同一个类中。
只测试一件事情的测试比可能在很多地方产生失败的测试更有益。如何排除外部因素对测试的影响?简单地说,就是把花费大、杂乱、不可靠、速度慢或复杂的资源 换成为你的测试起见自动产生的存根。例如,你可以用常量替换实际上非常复杂的计算,至少可以针对单个测试(这样做)。
存根解决了分配代价高昂的外部资源的问题。例如,在测试间共享类似数据库连接有用,但为测试起见从根本上避免使用数据库会更好。
显示如何存根化方法调用以及装配返回值。
范例 11.2: 存根化一个方法调用
require_once 'PHPUnit/Framework.php';
class StubTest extends PHPUnit_Framework_TestCase
{
public function testStub()
{
$stub = $this->getMock('SomeClass', array('doSomething'));
$stub->expects($this->any())
->method('doSomething')
->will($this->returnValue('foo'));
// 调用$stub->doSomething()会立刻返回“foo”。
}
}
?>
列出可用来为存根方法调用配置返回值的方法。
表11.2. 存根API
方法 | 含义 |
---|---|
PHPUnit_Framework_MockObject_Stub_Exception throwException(Exception $exception) | 设定对方法的所有调用都会跑出的异常。 |
PHPUnit_Framework_MockObject_Stub_Return returnValue(mixed $value) | 设定对方法的所有调用都返回$value 。 |
PHPUnit_Framework_MockObject_Stub_ConsecutiveCalls onConsecutiveCalls(mixed $value, ...) | 为方法的连续调用装配不同的返回值。 |
此外,你能自己编写存根并据此改善你的设计。通过单个façade(façade模式?)访问广泛使用的资源,这样就能方便地用存根替换资源。例如,你有个Database
对象,接口IDatabase
的实现,而不是让直接数据库访问散落在代码各处。然后就能创建一个实现了IDatabase
的存根并用于你的测试。你甚至可以创建一个选项,用于(选择)带存根数据库或真实的数据库运行测试,这样你的测试就既能在开发期间用于本地测试,又能用于真实数据库的综合测试。
需要存根化的功能趋向于集成在同一个对象中,提高了内聚性。通过使用单个内聚的接口引入各种功能,你就降低了同系统其他部分的耦合。