PHPUnit學習03---使用Mock對象解決測試依賴


本文目的

單元測試過程中經常會遇到被測試函數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));
	}
}

?>

執行結果:

clip_image002

上面的方法看似不復雜,只是繼承了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));//真實的調用,並斷言調用結果
	}
}

?>

 

上面的例子的執行結果如下:

clip_image004

我們來分析一下上面的代碼,

$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對象無能為力。

相關鏈接


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM