# 仿件對象(Mock Object)
將對象替換為能驗證預期行為(例如斷言某個方法必會被調用)的測試替身的實踐方法稱為*模仿(mocking)*。
可以用 *仿件對象(mock object)*“作為觀察點來核實被測試系統在測試中的間接輸出。通常,仿件對象還需要包括樁件的功能,因為如果測試尚未失敗則仿件對象需要向被測系統返回一些值,但是其重點還是在對間接輸出的核實上。因此,仿件對象遠不止是樁件加斷言,它是以一種從根本上完全不同的方式來使用的”(Gerard Meszaros)。
### 局限性:對預期的自動校驗
PHPUnit只會對在某個測試的作用域內生成的仿件對象進行自動校驗。諸如在數據供給器內生成或用`@depends` 標注注入測試的仿件對象,PHPUnit并不會自動對其進行校驗。
這有個例子:假設需要測試的當前方法,在例子中是 `update()`,確實在一個觀察著另外一個對象的對象中上被調用了。[Example?9.10, “被測系統(SUT)中 Subject 與 Observer 類的代碼”](# "Example?9.10.?被測系統(SUT)中 Subject 與 Observer 類的代碼")展示了被測系統(SUT)中 `Subject` 和 `Observer` 兩個類的代碼。
**Example?9.10.?被測系統(SUT)中 Subject 與 Observer 類的代碼**
~~~
<?php
class Subject
{
protected $observers = array();
protected $name;
public function __construct($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
public function attach(Observer $observer)
{
$this->observers[] = $observer;
}
public function doSomething()
{
// 做點什么。
// ...
// 通知觀察者。
$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);
}
}
// 其他方法。
}
class Observer
{
public function update($argument)
{
// 做點什么。
}
public function reportError($errorCode, $errorMessage, Subject $subject)
{
// 做點什么。
}
// 其他方法。
}
?>
~~~
[Example?9.11, “測試某個方法會以特定參數被調用一次”](# "Example?9.11.?測試某個方法會以特定參數被調用一次")展示了如何用仿件對象來測試 `Subject` 和 `Observer` 對象之間的互動。
首先用 `PHPUnit_Framework_TestCase` 類提供的 `getMock()` 方法建立 `Observer` 的仿件對象。由于給出了一個數組做為 `getMock()` 方法的第二(可選)參數,`Observer` 類只有 `update()` 方法會被替換為仿實現。
由于關注的是檢驗某個方法是否被調用,以及調用時具體所使用的參數,因此引入 `expects()` 與 `with()` 方法來指明此交互應該是什么樣的。
**Example?9.11.?測試某個方法會以特定參數被調用一次**
~~~
<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
public function testObserversAreUpdated()
{
// 為 Observer 類建立仿件對象,只模仿 update() 方法。
$observer = $this->getMockBuilder('Observer')
->setMethods(array('update'))
->getMock();
// 建立預期狀況:update() 方法將會被調用一次,
// 并且將以字符串 'something' 為參數。
$observer->expects($this->once())
->method('update')
->with($this->equalTo('something'));
// 創建 Subject 對象,并將模仿的 Observer 對象連接其上。
$subject = new Subject('My subject');
$subject->attach($observer);
// 在 $subject 對象上調用 doSomething() 方法,
// 預期將以字符串 'something' 為參數調用
// Observer 仿件對象的 update() 方法。
$subject->doSomething();
}
}
?>
~~~
`with()` 方法可以攜帶任何數量的參數,對應于被模仿的方法的參數數量。可以對方法的參數指定更加高等的約束而不僅是簡單的匹配。
**Example?9.12.?測試某個方法將會以特定數量的參數進行調用,并且對各個參數以多種方式進行約束**
~~~
<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
public function testErrorReported()
{
// 為 Observer 類建立仿件,對 reportError() 方法進行模仿
$observer = $this->getMockBuilder('Observer')
->setMethods(array('reportError'))
->getMock();
$observer->expects($this->once())
->method('reportError')
->with(
$this->greaterThan(0),
$this->stringContains('Something'),
$this->anything()
);
$subject = new Subject('My subject');
$subject->attach($observer);
// doSomethingBad() 方法應當會通過(observer的)reportError()方法
//向 observer 報告錯誤。
$subject->doSomethingBad();
}
}
?>
~~~
`withConsecutive()` 方法可以接受任意多個數組作為參數,具體數量取決于欲測試的調用。每個數組都都是對被仿方法的相應參數的一組約束,就像 `with()` 中那樣。
**Example?9.13.?測試某個方法將會以特定參數被調用二次**
~~~
<?php
class FooTest extends PHPUnit_Framework_TestCase
{
public function testFunctionCalledTwoTimesWithSpecificArguments()
{
$mock = $this->getMockBuilder('stdClass')
->setMethods(array('set'))
->getMock();
$mock->expects($this->exactly(2))
->method('set')
->withConsecutive(
array($this->equalTo('foo'), $this->greaterThan(0)),
array($this->equalTo('bar'), $this->greaterThan(0))
);
$mock->set('foo', 21);
$mock->set('bar', 48);
}
}
?>
~~~
`callback()` 約束用來進行更加復雜的參數校驗。此約束的唯一參數是一個 PHP 回調項(callback)。此 PHP 回調項接受需要校驗的參數作為其唯一參數,并應當在參數通過校驗時返回 `TRUE`,否則返回 `FALSE`。
**Example?9.14.?更加復雜的參數校驗**
~~~
<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
public function testErrorReported()
{
// 為 Observer 類建立仿件,模仿 reportError() 方法
$observer = $this->getMockBuilder('Observer')
->setMethods(array('reportError'))
->getMock();
$observer->expects($this->once())
->method('reportError')
->with($this->greaterThan(0),
$this->stringContains('Something'),
$this->callback(function($subject){
return is_callable(array($subject, 'getName')) &&
$subject->getName() == 'My subject';
}));
$subject = new Subject('My subject');
$subject->attach($observer);
// doSomethingBad() 方法應當會通過(observer的)reportError()方法
//向 observer 報告錯誤。
$subject->doSomethingBad();
}
}
?>
~~~
**Example?9.15.?測試某個方法將會被調用一次,并且以某個特定對象作為參數。**
~~~
<?php
class FooTest extends PHPUnit_Framework_TestCase
{
public function testIdenticalObjectPassed()
{
$expectedObject = new stdClass;
$mock = $this->getMockBuilder('stdClass')
->setMethods(array('foo'))
->getMock();
$mock->expects($this->once())
->method('foo')
->with($this->identicalTo($expectedObject));
$mock->foo($expectedObject);
}
}
?>
~~~
**Example?9.16.?創建仿件對象時啟用參數克隆**
~~~
<?php
class FooTest extends PHPUnit_Framework_TestCase
{
public function testIdenticalObjectPassed()
{
$cloneArguments = true;
$mock = $this->getMockBuilder('stdClass')
->enableArgumentCloning()
->getMock();
// 現在仿件將對參數進行克隆,因此 identicalTo 約束將會失敗。
}
}
?>
~~~
[Table?A.1, “約束條件”](# "Table?A.1.?約束條件")列出了可以應用于方法參數的各種約束,[Table?9.1, “匹配器”](# "Table?9.1.?匹配器")列出了可以用于指定調用次數的各種匹配器。
**Table?9.1.?匹配器**
| 匹配器 | 含義 |
|-----|-----|
| PHPUnit_Framework_MockObject_Matcher_AnyInvokedCount any() | 返回一個匹配器,當被評定的方法執行0次或更多次(即任意次數)時匹配成功。 |
| 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` 個執行的方法時匹配成功。 |
>[info] ### Note
> `at()` 匹配器的 `$index` 參數指的是對給定仿件對象的*所有方法的調用*的索引,從零開始。使用這個匹配器要謹慎,因為它可能導致測試由于與具體的實現細節過分緊密綁定而變得脆弱。
- PHPUnit 手冊
- 1. 安裝 PHPUnit
- 需求
- PHP 檔案包 (PHAR)
- Composer
- 可選的組件包
- 2. 編寫 PHPUnit 測試
- 測試的依賴關系
- 數據供給器
- 對異常進行測試
- 對 PHP 錯誤進行測試
- 對輸出進行測試
- 錯誤相關信息的輸出
- 3. 命令行測試執行器
- 命令行選項
- 4. 基境(fixture)
- setUp() 多 tearDown() 少
- 變體
- 基境共享
- 全局狀態
- 5. 組織測試
- 用文件系統來編排測試套件
- 用 XML 配置來編排測試套件
- 6. 有風險的測試
- 無用測試
- 意外的代碼覆蓋
- 測試執行期間產生的輸出
- 測試執行時長的超時限制
- 全局狀態篡改
- 7. 未完成的測試與跳過的測試
- 未完成的測試
- 跳過測試
- 用 @requires 來跳過測試
- 8. 數據庫測試
- 數據庫測試所支持的供應商
- 數據庫測試的難點
- 數據庫測試的四個階段
- PHPUnit 數據庫測試用例的配置
- 理解 DataSet(數據集)和 DataTable(數據表)
- 數據庫連接 API
- 數據庫斷言 API
- 常見問題(FAQ)
- 9. 測試替身
- Stubs (樁件)
- 仿件對象(Mock Object)
- Prophecy
- 對特質(Trait)與抽象類進行模仿
- 對 Web 服務(Web Services)進行上樁或模仿
- 對文件系統進行模仿
- 10. 測試實踐
- 在開發過程中
- 在調試過程中
- 11. 代碼覆蓋率分析
- 用于代碼覆蓋率的軟件衡量標準
- 包含與排除文件
- 略過代碼塊
- 指明要覆蓋的方法
- 邊緣情況
- 12. 測試的其他用途
- 敏捷文檔
- 跨團隊測試
- 13. Logging (日志記錄)
- 測試結果 (XML)
- 測試結果 (TAP)
- 測試結果 (JSON)
- 代碼覆蓋率 (XML)
- 代碼覆蓋率 (TEXT)
- 14. 擴展 PHPUnit
- 從 PHPUnit_Framework_TestCase 派生子類
- 編寫自定義斷言
- 實現 PHPUnit_Framework_TestListener
- 從 PHPUnit_Extensions_TestDecorator 派生子類
- 實現 PHPUnit_Framework_Test
- A. 斷言
- assertArrayHasKey()
- assertClassHasAttribute()
- assertArraySubset()
- assertClassHasStaticAttribute()
- assertContains()
- assertContainsOnly()
- assertContainsOnlyInstancesOf()
- assertCount()
- assertEmpty()
- assertEqualXMLStructure()
- assertEquals()
- assertFalse()
- assertFileEquals()
- assertFileExists()
- assertGreaterThan()
- assertGreaterThanOrEqual()
- assertInfinite()
- assertInstanceOf()
- assertInternalType()
- assertJsonFileEqualsJsonFile()
- assertJsonStringEqualsJsonFile()
- assertJsonStringEqualsJsonString()
- assertLessThan()
- assertLessThanOrEqual()
- assertNan()
- assertNull()
- assertObjectHasAttribute()
- assertRegExp()
- assertStringMatchesFormat()
- assertStringMatchesFormatFile()
- assertSame()
- assertStringEndsWith()
- assertStringEqualsFile()
- assertStringStartsWith()
- assertThat()
- assertTrue()
- assertXmlFileEqualsXmlFile()
- assertXmlStringEqualsXmlFile()
- assertXmlStringEqualsXmlString()
- B. 標注
- @author
- @after
- @afterClass
- @backupGlobals
- @backupStaticAttributes
- @before
- @beforeClass
- @codeCoverageIgnore*
- @covers
- @coversDefaultClass
- @coversNothing
- @dataProvider
- @depends
- @expectedException
- @expectedExceptionCode
- @expectedExceptionMessage
- @expectedExceptionMessageRegExp
- @group
- @large
- @medium
- @preserveGlobalState
- @requires
- @runTestsInSeparateProcesses
- @runInSeparateProcess
- @small
- @test
- @testdox
- @ticket
- @uses
- C. XML 配置文件
- PHPUnit
- 測試套件
- 分組
- 為代碼覆蓋率包含或排除文件
- Logging (日志記錄)
- 測試監聽器
- 設定 PHP INI 設置、常量、全局變量
- 為 Selenium RC 配置瀏覽器
- D. 升級
- E. 索引
- F. 參考書目
- G. 版權