【PHP】PHPUnit单元测试利器:PHP Mock的使用方法
由于环境依赖关系,或者是特殊环境的构造要求,这就可能导致我们在测试环境下做验证是很困难的。
当我们无法直接使用的真实被依赖模块时,我们可以用“测试替身”(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: 待插桩的类
<?Php class SomeClass { public function doSomething() { // Do something. } } ?>
例 10.2: 对函数调用进行插桩并指定返回值
<?php 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来配置生成的测试替身类
<?php 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: 对指定方法打桩,并让其返回指定传入参数
<?php 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: 对指定方法打桩并使其返回打桩对象本身
<?php 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里配置的值
<?php 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: 对指定方法打桩,并使其返回值为指定函数调用的返回值
<?php 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: 对指定方法打桩,并使其返回值按指定序列逐次返回
<?php 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都是测试系统的一部分
<?php 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: 测试指定方法,该方法被调用一次,并检查调用时的参数
<?php 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: 测试指定方法,并使用不同的方式对该方法调用时的参数进行约束
<?php 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: 测试抽象类的非抽象方法
<?php 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: 测试一个方法,其获取到的参数与调用时的参数一致
<?php 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对象
<?php 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. } } ?>