本文目的
單元測試過程中經常會遇到被測試函數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/