由于环境依赖关系,或者是特殊环境的构造要求,这就可能导致我们在测试环境下做验证是很困难的。
当我们无法直接使用的真实被依赖模块时,我们可以用 “测试替身”(Test Double)来代替。这个测试替身不需要与真实的被依赖模块有相同的行为,它只需要提供和真实的被依赖模块有相同的API就行了。
PHPUnit提供的getMock($className)方法可以自动生成一个对象,而这个对象就可以作为原来那个类的测试替身。这个测试替身可以用在任何需要它的地方。
默认情况下,原类的所有方法都被一个虚拟的实现替代,这个实现仅仅是返回NULL(不会调用原类中的对应方法)。你可以使用will($this->returnValue())方法来配置其被调用时的返回值,从而将这些虚拟的实现具体化。
限制:final,private及static方法不能被插桩或者模拟,在测试替身中,这些方法会保留其原有的实现。
警告:需要注意的是其参数管理已经被修改了。原先的实现是拷贝所有的参数,这样就无法判断传到函数里的对象是否是相同的了。例10.14显示了新的实现所带来的好处,例10.15显示了如何切换回到以前的行为(见本文最后)。
桩
打桩就是使用测试替身对象来替换原有的对象,而这个测试替身的返回值是可配置的。你可以使用桩来替换测试所依赖的真实模块,这样就可以在测试的间接输入中得到一个控制点,这样就可以让测试流程不要再继续执行下去,因为不这样的话测试可能无法正常执行下去。
例10.2显示如何对方法调用进行打桩以及如何设置该方法的返回值。我们首先使用PHPUnit_Framework_TestCase类所提供的getMock()方法来建立一个stub对象,这个对象就如例10.1中的SomeClass对象一样。 然后,我们使用PHPUnit提供的一系列接口来指定桩的行为。 从本质上讲,这意味着你不需要创建多个临时对象,并把它们绑在一起。 相反,使用示例中所示链式的方法调用将导致代码更易读。
例 10.1: 待插桩的类
class SomeClass {
public function doSomething() {
// Do something.
}
}
?>
例 10.2: 对函数调用进行插桩并指定返回值
require_once "SomeClass.php";
class StubTest extends PHPUnit_Framework_TestCase {
public function testStub() {
// Create a stub for the SomeClass class.
$stub = $this->getMock("SomeClass");
// Configure the stub.
$stub->expects($this->any())
->method("doSomething")
->will($this->returnValue("foo"));
// Calling $stub->doSomething() will now return
// "foo".
$this->assertEquals("foo", $stub->doSomething());
}
}
?>
在以上代码中,当我们调用getMock()方法时,PHPUnit会自动生成一个新的类,并且通过这个类来实现预期的行为。这个测试替身类是可以通过可选参数来配置的。
默认情况下,除非是通过will($this->returnValue())来指定了返回值,否则都是返回NULL。
当第二个参数(可选)被指定时,表明只有array数组里配置的方法才会被替换,其他方法依然保留原有的实现。
第三个参数(可选)可以指定一个参数数组并传给原类的构造函数(构造函数在默认情况下是不会被虚拟实现替换掉的)。
第四个参数(可选)可以为测试替身类指定一个类名。
第五个参数(可选)可用于禁用调用原类的构造函数 。
第六个参数(可选)可用于禁用原类的复制构造函数的调用 。
第七个参数(可选)可以用于禁止测试替身类生成过程中对__autoload()的调用。
另外一个可选择的方法是使用Mock Builder API来配置生成的测试替身类。如例10.3.以下列出Mock Builder提供的接口:
setMethods(array $methods)可以用于Mock Builder对象需要替换的方法,其他方法仍然保留原类的实现。
调用setConstructorArgs(array $args)方法可以指定传入原类的构造函数的参数(原类的构造函数在默认情况下是不会被虚拟化的)。
setMockClassName($name)可用于指定测试替身类的类名。
disableOriginalConstructor()可用于禁调用原类的构造函数。
disableOriginalClone()可以用来禁止原类的复制构造函数的调用。
disableAutoload()可以用于禁止生成测试替身类时__autoload()的调用。
例 10.3: 用 Mock Builder API来配置生成的测试替身类
require_once "SomeClass.php";
class StubTest extends PHPUnit_Framework_TestCase {
public function testStub() {
// Create a stub for the SomeClass class.
$stub = $this->getMockBuilder("SomeClass")->disableOriginalConstructor()->getMock();
// Configure the stub.
$stub->expects($this->any())
->method("doSomething")
->will($this->returnValue("foo"));
// Calling $stub->doSomething() will now return
// "foo".
$this->assertEquals("foo", $stub->doSomething());
}
}
?>
有时你可能希望被打桩的方法的返回值是某个传入的参数,这时可以使用例10.4中所示的通过替换resutnValue()为returnArgument()方法来实现:
例 10.4: 对指定方法打桩,并让其返回指定传入参数
require_once "SomeClass.php";
class StubTest extends PHPUnit_Framework_TestCase {
public function testReturnArgumentStub() {
// Create a stub for the SomeClass class.
$stub = $this->getMock("SomeClass");
// Configure the stub.
$stub->expects($this->any())
->method("doSomething")
->will($this->returnArgument(0));
// $stub->doSomething("foo") returns "foo"
$this->assertEquals("foo", $stub->doSomething("foo"));
// $stub->doSomething("bar") returns "bar"
$this->assertEquals("bar", $stub->doSomething("bar"));
}
}
?>
有时需要被打桩的方法返回被打桩的类的引用,这时可以使用returnSelf()方法来实现,如例10.5所示:
例 10.5: 对指定方法打桩并使其返回打桩对象本身
require_once "SomeClass.php";
class StubTest extends PHPUnit_Framework_TestCase {
public function testReturnSelf() {
// Create a stub for the SomeClass class.
$stub = $this->getMock("SomeClass");
// Configure the stub.
$stub->expects($this->any())
->method("doSomething")
->will($this->returnSelf());
// $stub->doSomething() returns $stub
$this->assertSame($stub, $stub->doSomething());
}
}
?>
有时被打桩的方法需要针对不同的参数返回不同的值,这时可以使用returnValueMap()创建一个map来关联参数和返回值。如例10.6所示:
例 10.6: 对指定方法打桩,并使其返回值为 map里配置的值
require_once "SomeClass.php";
class StubTest extends PHPUnit_Framework_TestCase {
public function testReturnValueMapStub() {
// Create a stub for the SomeClass class.
$stub = $this->getMock("SomeClass");
// Create a map of arguments to return values.
$map = array(
array("a", "b", "c", "d"),
array("e", "f", "g", "h")
);
// Configure the stub.
$stub->expects($this->any())
->method("doSomething")
->will($this->returnValueMap($map));
// $stub->doSomething() returns different values depending on
// the provided arguments.
$this->assertEquals("d", $stub->doSomething("a", "b", "c"));
$this->assertEquals("h", $stub->doSomething("e", "f", "g"));
}
}
?>
当被打桩的方法需要返回一个计算值,而不是固定值(参见returnValue())或者指定参数(参见returnArgument)时,这时可以使用returnCallback()来指定被打桩方法的回调函数。如例10.7:
例 10.7: 对指定方法打桩,并使其返回值为指定函数调用的返回值
require_once "SomeClass.php";
class StubTest extends PHPUnit_Framework_TestCase {
public function testReturnCallbackStub() {
// Create a stub for the SomeClass class.
$stub = $this->getMock("SomeClass");
// Configure the stub.
$stub->expects($this->any())
->method("doSomething")
->will($this->returnCallback("str_rot13"));
// $stub->doSomething($argument) returns str_rot13($argument)
$this->assertEquals("fbzrguvat", $stub->doSomething("something"));
}
}
?>
另外一种更简单的设置回调函数的方法是:设定期望返回值列表。你可以使用onConsecutiveCalls()来实现这个功能,如例10.8所示:
例 10.8: 对指定方法打桩,并使其返回值按指定序列逐次返回
require_once "SomeClass.php";
class StubTest extends PHPUnit_Framework_TestCase {
public function testOnConsecutiveCallsStub() {
// Create a stub for the SomeClass class.
$stub = $this->getMock("SomeClass");
// Configure the stub.
$stub->expects($this->any())
->method("doSomething")
->will($this->onConsecutiveCalls(2, 3, 5, 7));
// $stub->doSomething() returns a different value each time
$this->assertEquals(2, $stub->doSomething());
$this->assertEquals(3, $stub->doSomething());
$this->assertEquals(5, $stub->doSomething());
}
}
?>
抛出异常(略)
Mock对象
Mocking是指使用测试替身来替换原有对象并对期望作检查,例如断言某个方法被调用了。
你可以使用mock对象作为观察点来验证测试过程中的间接输出。通常来说,mock对象包含测试桩的功能,因此它必须要在测试过程中有返回值(如何测试未失败的话),但更重要的是对于间接输出的验证。因此,mock对象远不止是一个测试桩加一个断言这么简单,它是用完全不同的方式。
注意:要 mock 的类如果不存在的话,phpunit 会生成一个空的同名的类。如果要使用原来的类的话,需要把声明该类的文件包含进来,不然的话就可能会提示 "Fatal error:Call to undefined method XXX::xxx() in xxx.php on line xxx" 这类错误了。
以下是一个例子,假设我们需要测试测试例子中的update()方法,这个方法是被另外一个对象的观察者调用的,如例10.10:
例 10.10: 类 Subject 和 Observer 都是测试系统的一部分
class Subject {
protected $observers = array();
public function attach(Observer $observer) {
$this->observers[] = $observer;
}
public function doSomething() {
// Do something.
// ...
// Notify observers that we did something.
$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);
}
}
// Other methods.
}
class Observer {
public function update($argument) {
// Do something.
}
public function reportError($errorCode, $errorMessage, Subject $subject) {
// Do something
}
// Other methods.
}
?>
例10.11显示了如何使用mock对象来测试Subject和Observer对象之间的相互作用:
例 10.11: 测试指定方法,该方法被调用一次,并检查调用时的参数
class SubjectTest extends PHPUnit_Framework_TestCase {
public function testObserversAreUpdated() {
// Create a mock for the Observer class,
// only mock the update() method.
$observer = $this->getMock("Observer", array("update"));
// Set up the expectation for the update() method
// to be called only once and with the string "something"
// as its parameter.
$observer->expects($this->once())
->method("update")
->with($this->equalTo("something"));
// Create a Subject object and attach the mocked
// Observer object to it.
$subject = new Subject();
$subject->attach($observer);
// Call the doSomething() method on the $subject object
// which we expect to call the mocked Observer object"s
// update() method with the string "something".
$subject->doSomething();
}
}
?>
with()方法可以有任意个参数,对应与被mocked的方法的参数个数,你可以对调用参数使用更加高级的的约束,如:
例 10.12: 测试指定方法,并使用不同的方式对该方法调用时的参数进行约束
class SubjectTest extends PHPUnit_Framework_TestCase {
public function testErrorReported() {
// Create a mock for the Observer class, mocking the
// reportError() method
$observer = $this->getMock("Observer", array("reportError"));
$observer->expects($this->once())
->method("reportError")
->with($this->greaterThan(0),
$this->stringContains("Something"),
$this->anything());
$subject = new Subject();
$subject->attach($observer);
// The doSomethingBad() method should report an error to the observer
// via the reportError() method
$subject->doSomethingBad();
}
}
?>
表4.3中所示的方法可以用来约束被mock方法的参数,表10.1中的匹配可以用来指定方法被调用的次数:
表 10.1。 Machers
匹配
含义
PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount any()
返回一个匹配相匹配时,它评估的方法被执行零次或多次。
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匹配时调用的方法对其进行评估。注意:mock 对象的任意方法被调用时,index 都会加 1。
getMockForAbstractClass()方法可以为抽象类返回一个mock对象,所有的抽象方法都会被mock,而非抽象方法则不会被mock,这样我们就可以测试一个抽象类的非抽象方法了。如例10.13所示:
例 10.13: 测试抽象类的非抽象方法
abstract class AbstractClass
{
public function concreteMethod ( )
{
return $this -> abstractMethod ( ) ;
}
public abstract function abstractMethod ( ) ;
}
class AbstractClassTest extends PHPUnit_Framework_TestCase
{
public function testConcreteMethod ( )
{
$stub = $this -> getMockForAbstractClass ( "AbstractClass" ) ;
$stub -> expects ( $this -> any ( ) )
-> method ( "abstractMethod" )
-> will ( $this -> returnValue ( TRUE ) ) ;
$this -> assertTrue ( $stub -> concreteMethod ( ) ) ;
}
}
?>
例 10.14: 测试一个方法,其获取到的参数与调用时的参数一致
class FooTest extends PHPUnit_Framework_TestCase {
public function testIdenticalObjectPassed() {
$expectedObject = new stdClass();
$mock = $this->getMock("stdClass", array("foo"));
$mock->expects($this->once())
->method("foo")
->with($this->identicalTo($expectedObject));
$mock->foo($expectedObject);
}
}
?>
例 10.15: 当允许拷贝参数功能开启时,创建 Mock对象
class FooTest extends PHPUnit_Framework_TestCase {
public function testIdenticalObjectPassed() {
$cloneArguments = true;
$mock = $this->getMock("stdClass",
array(),
array(),
"",
FALSE,
TRUE,
TRUE,
$cloneArguments);
// or using the mock builder
$mock = $this->getMockBuilder("stdClass")
->enableArgumentCloning()
->getMock();
// now your mock clones parameters so the identicalTo constraint will fail.
}
}
?>