面向對象的編程之所以豐富多彩,部分是由于對象間的相互聯系與作用。一個單一的對象就能封裝一個復雜的子系統,使那些很復雜的操作能夠通過一些方法的調用而簡化。(無所不在的數據庫連接就是這樣的一個對象實例。)
然而經常有這樣的情況,對象間的交互性是如此復雜以至于我們不得不面對類似“先有雞還是先有蛋”這樣傷腦筋的問題:如何創建并測試這樣一個對象,他要么依賴于很多已創建的對象,要么依賴于其他一些難以意識到的情況,如整個數據庫的創建和測試。
問題
如何分隔并測試一個與其他對象和資源有關的代碼段?又如何再創建一個或多個對象、程序來驗證你的代碼能正常運行?
解決方案
當用situ(或在一個仿真的程序環境中)測試一個對象代價不菲或困難重重時,就可用偽對象來模擬這個行為。偽對象有同真實對象一樣的接口,但卻能提供預編譯響應,能跟蹤方法調用,并驗證調用次序。
偽對象是測試的“特別力量”。他們被秘密訓練,滲透進目標代碼,模擬并監視通信方式,回報結果。偽對象有助于查找和消除程序漏洞并能支持更多正常調試環境下的“防危險”操作。
注:The ServerStub
偽對象模式是另一種測試模式ServerStub的擴展。ServerStub模式替代一個資源并返回其方法所調用的相應值。當其參與指定次序的方法的調用時ServerStub就成了偽對象。
其并非是一個設計模式
本章與其他章不同,因為偽對象是一個測試模式而不是設計模式。這類似于一個附加的章節,但對它的使用 確實很值得你納入到編碼進程中。另一個不同是我們不再關注這個模式如何編碼之類的基礎問題,而是強調 如何在SimpleTest中使用偽對象。
本章先舉一個非常簡單的例子來示范SimpleTest下偽對象的基本機制。然后向你演示如何使用偽對象幫助重構已有代碼與如何測試新的解決方案。
樣本代碼
偽對象是對象在測試中的一個替代品,用它測試代碼更加簡便。例如,替代一個真實的數據連接——這個真實的數據連接由于一些原因而不能實際連接——你就可以創建一個偽對象來模擬。這意味著偽對象需要準確地回應代碼中所調用的相同的應用程序接口。
讓我們創建一個偽對象來替代一個簡單的名為Accumulator的類,這是一個求和的類。如下是最初的Accumulator類:
| // PHP4 |
| function calc_total($items, &$sum) { foreach($items as $item) { $sum->add($item); } } function calc_tax(&$amount, $rate=0.07) { return round($amount->total() * $rate,2); } |
第一個函數calc_total()用一個累加的動作求一系列值的和。下面是簡單的測試:
| class MockObjectTestCase extends UnitTestCase { function testCalcTotal() { $sum =& new Accumulator; calc_total(array(1,2,3), $sum); $this->assertEqual(6, $sum->total()); } } |
讓我們關注第二個例子。假設實現一個真實的累加動作的代價很大。那么用一個簡單的對象來替代它并回應相關代碼就是很好的做法了。使用SimpleTest,你可以用如下代碼創建一個偽累加動作:
| Mock::generate(‘Accumulator’); class MockObjectTestCase extends UnitTestCase { // ... function testCalcTax() { $amount =& new MockAccumulator($this); $amount->setReturnValue(‘total’,200); $this->assertEqual( 14, calc_tax($amount)); } } |
為了使用偽對象,具有代表性的做法是你親自寫一個新類(并不要求馬上做)。幸運的是,SimpleTest有一種容易的手段來實現 Mock::generate() 方法。
在上面的例子中,這種手段創建了一個名為MockAccumulator的類來響應所有Accumulator類的方法。另外,偽累加的動作還有其他手段來操作偽對象自身的實例。例如 setReturnValue()。給出一個方法名和一個值,
setReturnValue()就可以改變偽對象而給出對應方法所調用的值。因此,這條語句$amount->setReturnValue(‘total’, 200)返回200而不論何時調用了total()方法。
一旦進行完初始化工作后,你可以傳遞MockAccumulator類到calc_tax()函數來演示一個在真實的Accumulator對象空間中的動作。
如果你止步于此——即用一個對象來返回所調用函數的“封裝”響應——你只是使用了ServerStub模式。 用偽對象來驗證方法的調用不限于此,因為它可以不限次序與次數。
下面是一個通過對象來驗證“數據流”的例子:
| class MockObjectTestCase extends UnitTestCase { // ... function testCalcTax() { $amount =& new MockAccumulator($this); $amount->setReturnValue(‘total’,200); $amount->expectOnce(‘total’); $this->assertEqual( 14, calc_tax($amount)); $amount->tally(); } } |
這里expectOnce()方法使用了一個字符串,它包含你想調用的方法名 。而tally()實際上用來檢查你的想法是否實現。這里,如果MockAccumulator::total()只調用一次或不調用,測試將失敗。
在很多情況下你可以使用偽對象的”跟蹤”特性。例如,如果你傳遞一個具有三個值的數組到calc_total(),Accumulator::add()是否也如你所想的調用了三次呢?
| class MockObjectTestCase extends UnitTestCase { // ... function testCalcTotalAgain() { $sum =& new MockAccumulator($this); $sum->expectOnce(‘add’); calc_total(array(1,2,3), $sum); $sum->tally(); } } |
那,這里發生了什么?傳遞調用的測試失敗。SimpleTest的錯誤消息如下所示:
| MockObject PHP4 Unit Test 1) Expected call count for [add] was [1] got [3] at line [51] in testcalctotalagain in mockobjecttestcase FAILURES!!! Test cases run: 1/1, Passes: 2, Failures: 1, Exceptions: 0 |
錯誤消息指出了盡管add() 方法被調用三次,但expectOnce()卻一次也沒用到。取代expectOnce()的可行方法是使用expectCallCount()。
| class MockObjectTestCase extends UnitTestCase { // ... function testCalcTotalAgain() { $sum =& new MockAccumulator($this); $sum->expectCallCount(‘add’, 3); calc_total(array(1,2,3), $sum); $sum->tally(); } } |
偽對象扮演了一個演員的角色——這個角色由SeverStub提供合理的測試數據來響應方法的調用——并且作為一個評判的角色,驗證所調用的方法是否符合預想。
重構已有程序
下面讓我們用偽對象來幫助重構一個已有程序。考慮一個簡單的腳本,它可以模擬你在無數的PHP程序中所期望的行為:例如一個當檢查到你未登錄時要求登錄的頁面;與此類似的還有表單處理頁面;它能在成功登錄后顯示不同內容并提供登出的功能。 讓我們寫一個這樣的頁面。首先,對還未登錄的用戶顯示一個登錄表單。
| <html> <body> <form method=”post”> Name:<input type=”text” name=”name”> Password:<input type=”password” name=”passwd”> <input type=”submit” value=”Login”> </form> </body> </html> |
接著,顯示登錄成功后的內容:
| <html> <body>Welcome <?php echo $_SESSION[‘name’]; ?> <br>Super secret member only content here. <a href=”<?php echo SELF; ?>?clear”>Logout</a> </body> </html> |
加入表單處理的功能,session(會話)開始,還有登出的功能,整體看起來應該類似這樣:
| session_start(); |
例如,如果你直接就用了$_SESSION,即意味著只有一種途徑可以測試這個代碼,就是改變$_SESSION。如果你忘了將$_SESSION改回先前已知的狀態,各種測試間就會互相干擾。
一個好的解決方法是封裝$_SESSION到另一個類中,傳遞所封裝類的實例到任何想要訪問$_SESSION的對象。如果你創建了一個已封裝對象的偽對象用于測試,你能夠完全控制對象對所調用方法的響應(就像ServerStub那樣)并且你能核實它是如何調用的(那正是創建偽對象的目的)。
新聞熱點
疑難解答