【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()的調用。

<?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. 
    }
}
?>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章