由於環境依賴關係,或者是特殊環境的構造要求,這就可能導致我們在測試環境下做驗證是很困難的。
當我們無法直接使用的真實被依賴模塊時,我們可以用“測試替身”(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.
}
}
?>