本文目的
單元測試過程中經常會遇到被測試函數A依賴另一個函數B,但是B已經完全測試過,沒有必要在測試A的時候重復測試B。如何去除這種不必要的測試呢?本文探討了如何手動解決測試依賴,更進一步地,結合PHPUnit的Mock API,提出更加優雅,高效的解決方案。
一個例子
假設有一個訂單管理類OrderManager,它的私有變量中,有一個OrderDao,當插入訂單時,首先OrderManager會檢查內參數是否合法,然后調用OrderDao的insert方法,將Order對象插入到數據庫中。現在,假設已經測底的對OrderDao的所有方法進行了單元測試,需要測試OrderManager相關方。此時,就產生了測試依賴的問題。具體代碼如下:
<?php
class OrderDao{
public function insert($aOrder){
if($aOrder['id'] == 'order_id_already_existing'){
return -1;
}
// 這個方法只是簡單的模擬,不作實質的數據庫操作
print "connect to db\n";
print "execute query\n";
print "1 row effected\n";
print "insert order {$aOrder['id']} successfully\n";
return 0;
}
}
class OrderManager{
private $_oOrderDao;
public function __construct(OrderDao $oOd){
$this->_oOrderDao = $oOd;
}
public function insertOrder($aOrder){
if(array_key_exists('id', $aOrder) && $aOrder['id'] != ''){
print "order {$aOrder['id']} is valide!\n";
if($this->_oOrderDao->insert($aOrder) == 0){
print "call dao insert successfully\n";
return true;
}
else{
print "insert error!\n";
return false;
}
}else{
print "order {$aOrder['id']} is invalide!\n";
return false;
}
}
}
?>
假設上面的文件中,OrderDao已經被測試測試,現在需要測試OrderManager::insertOrder方法。這個方法調用了OrderDao::insert方法。下面,先看看手動創建一個mock(mock的中文意識是“模仿”)類進行單元測試的方案。
手動創建Mock
創建一個新的類,稱為OrderDaoMock,繼承類OrderDao,方法實現時采用一些簡單,方便,無意義的實現,如下(ut_order_demo_manual.php):
<?php
require_once 'order_demo.php';
class OrderDaoMock extends OrderDao{
public function insert($aOrder){
print "Mock Info: insert order {$aOrder['id']} successfully\n";
return 0;
}
}
class OrderDemo_TestCase extends PHPUnit_Framework_TestCase{
public function testNullIdOrder(){
$oOm = new OrderManager(new OrderDaoMock());
$aOrder = array('id'=>'');
$this->assertFalse($oOm->insertOrder($aOrder));
}
public function testNoIdOrder(){
$oOm = new OrderManager(new OrderDaoMock());
$aOrder = array();
$this->assertFalse($oOm->insertOrder($aOrder));
}
public function testSuccessInsertOrder(){
$oOm = new OrderManager(new OrderDaoMock());
$aOrder = array('id'=>'bourneli123456789');
$this->assertTrue($oOm->insertOrder($aOrder));
}
}
?>
執行結果:
上面的方法看似不復雜,只是繼承了OrderDao類,實現了一個啞方法insert。但是,設想如果在真實的系統中,被測試的方法會涉及到許多其他對象,如果每個對象都手動創建一個mock類,工作量還是十分大的。還好,PHPUnit提供了Mock類的API,可以方便的創建這些mock類。
PHPUnit的Mock API
現在,我們看看使用PHPUint的Mock API的版本(ut_order_demo_mock.php):
<?php
require_once 'order_demo.php';
class OrderDemo_TestCase extends PHPUnit_Framework_TestCase
{
public function testNullIdOrder(){
//自動創建一個集成OrderDao的mock對象
$oMockOrderDao = $this->getMock('OrderDao');
//期望不要調用這個對象的insert方法,如果調用,就會報錯
$oMockOrderDao->expects($this->never())->method('insert');
$oOm = new OrderManager($oMockOrderDao);
$aOrder = array('id'=>'');
$this->assertFalse($oOm->insertOrder($aOrder));
}
public function testNoIdOrder(){
$oMockOrderDao = $this->getMock('OrderDao');
$oMockOrderDao->expects($this->never())->method('insert');
$oOm = new OrderManager($oMockOrderDao);
$aOrder = array();
$this->assertFalse($oOm->insertOrder($aOrder));
}
public function testSuccessInsertOrder(){
$aOrder = array('id'=>'bourneli123456789');
$oMockOrderDao = $this->getMock('OrderDao');
$oMockOrderDao->expects($this->once())
->method('insert');
$oOm = new OrderManager($oMockOrderDao);
$this->assertTrue($oOm->insertOrder($aOrder));
}
public function testOrderIdExisting(){
$aOrder = array('id'=>'order_id_already_existing dfd');
//自動創建一個集成OrderDao的mock對象
$oMockOrderDao = $this->getMock('OrderDao');
//期望調用這個對象insert方法,次數任意。在調用時,輸入必須是$aOrder對象,
//返回必須是0。如果不滿足這種期望,將會報錯。
$oMockOrderDao->expects($this->any())
->method('insert')
->with($aOrder)
->will($this->returnValue(0));
$oOm = new OrderManager($oMockOrderDao);
$this->assertTrue($oOm->insertOrder($aOrder));//真實的調用,並斷言調用結果
}
}
?>
上面的例子的執行結果如下:
我們來分析一下上面的代碼,
$oMockOrderDao = $this->getMock('OrderDao');
上面這段代碼就為我們完成了手動創建OrderDaoMock類的工作。
$oMockOrderDao->expects($this->any())->method('insert')
->with($aOrder)->will($this->returnValue(0));
上面這段代碼代碼為我們完成了四個針對mock對象調用的斷言:
1)調用insert方法;
2)調用任意次;
3)調用時,輸入參數必須是$aOrder對象;
4)調用結束后,返回參數必須是0.
Mock API給了我們很大的自由度,可以隨意操作mock對象的行為,使得用mock進行單元測試十分便捷。根據上面的例子親自動手實踐,你會很容易理解mock對象的原理和作用。
Mock對象
為什么需要mock對象呢?有時候,很難測試被測系統(System Under Test,“被測系統”以下簡稱SUT),因為SUT依賴一些不能在測試環境使用的組件。這些組件有可能不可用(如第三方系統),或者他們不能返回測試中期望的結果,或者是這些組件執行后會帶來負面效果(如修改數據庫中的數據)。這時候,就需要mock對象來解決這些問題。Mock對象提供相同的API,供SUT調用,使得SUT可以正常運轉。如果希望在測試中大范圍的使用mock對象,對程序開發而言也有要求,程序開發過程中必須依照高內聚,底耦合的策略,並且盡量使用接口編程,這樣mock類才可以去模仿——通過繼承和多態,否則mock對象沒有用武之地。這一點,也暴露出了mock類的短板,mock只能模擬類中的public,protected函數,如果是static, private或final函數,mock對象無能為力。
相關鏈接
- PHPUnit Mock API官方介紹 http://www.phpunit.de/manual/3.4/en/test-doubles.html
- PHPUnit Mock API 使用介紹http://codeutopia.net/blog/2009/06/26/unit-testing-4-mock-objects-and-testing-code-which-uses-the-database/


