【PHP】PHPUnit单元测试利器:PHP Mock的使用方法

由于环境依赖关系,或者是特殊环境的构造要求,这就可能导致我们在测试环境下做验证是很困难的。

当我们无法直接使用的真实被依赖模块时,我们可以用“测试替身”(Test Double)来代替。这个测试替身不需要与真实的被依赖模块有相同的行为,它只需要提供和真实的被依赖模块有相同的API就行了。

PHPUnit提供的getMock($className)方法可以自动生成一个对象,而这个对象就可以作为原来那个类的测试替身。这个测试替身可以用在任何需要它的地方。

默认情况下,原类的所有方法都被一个虚拟的实现替代,这个实现仅仅是返回NULL(不会调用原类中的对应方法)。你可以使用will($this->returnValue())方法来配置其被调用时的返回值,从而将这些虚拟的实现具体化。

限制:final,privatestatic方法不能被插桩或者模拟,在测试替身中,这些方法会保留其原有的实现。

警告:需要注意的是其参数管理已经被修改了。原先的实现是拷贝所有的参数,这样就无法判断传到函数里的对象是否是相同的了。例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对象来测试SubjectObserver对象之间的相互作用:

例 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. 
    }
}
?> 

posted on 2012-10-31 12:03  jeJee  阅读(10088)  评论(0编辑  收藏  举报

导航