概念: 測試優先設計
本準則討論「測試優先設計」。「測試優先設計」是指在撰寫和測試程式碼之前先建立測試 Script。您可以持續運用這項技術,直到全部測試完畢為止。
關係
相關元素
主要說明

簡介

測試設計是以各種工作成果的資訊來建立,包括設計工作成果,例如使用案例實現化、設計模型或分類器介面。測試是在元件建立之後執行。通常就在執行測試之前建立測試設計 - 大約在建立軟體設計工作成果之後。下列圖 1 顯示一個範例。在這裡,測試設計有時是在實作將近結束時才開始。測試設計會汲取元件設計的結果。從「實作」至「測試執行」的箭頭表示必須完成實作,才能執行測試。

生命週期圖中的測試設計位置

圖 1:「測試設計」通常在生命週期的後期執行

但這不一定。雖然必須等到元件實作完成時才能執行測試,但可以提早設計測試。在設計工作成果完成之後,隨即可以展開。甚至可以和元件設計同步進行,如下圖所示:

測試優先設計圖

圖 2:「測試優先設計」可同步進行測試設計和軟體設計

以這種方式將測試工作「逆流而上」搬移,通常稱為「測試優先設計」。有何優點呢?

  1. 不論您如何小心地設計軟體,難免出錯。您可能遺漏重大事實。或因為您的特殊癖好,以至於沒有發現某些替代方案。或者,只是因為太累,因而疏忽某些事情。請其他人檢討您的設計工作成果會有幫助。他們可能知道您遺漏的事實,或看到您疏忽的事情。這些人和您的觀點最好不同;以不同的角度看設計,他們可以看到您遺漏的事物。

    根據經驗顯示,測試觀點是一項關鍵。也相當具體。在軟體設計期間,很容易將一個特定欄位認為是「顯示現行客戶的稱呼」,然後未加思索就採取行動。如果有一位客戶是海軍退役,後來又取得法律學位,但堅持自己的稱呼是「退役上尉 Morton H. Throckbottle 先生」,則在測試設計期間,您必須明確決定欄位將顯示什麼內容。到底該稱呼他為「上尉」還是「先生」?

    如果測試設計延後到測試執行前,如圖 1 所示,您可能只會浪費金錢。軟體設計的錯誤要等到測試設計時才會浮現,屆時,某位測試人員會說「你知道嗎?我認識這傢伙,是海軍退伍的...」、建立 "Morton" 測試,然後才發現問題。現在,必須重寫一部份或已全部完成的實作,也必須更新設計工作成果。在實作開始之前先抓出問題會比較划算。

  2. 有些錯誤可能在測試設計之前發現。否則將由「實作人員」發現。這樣還是不好。實作一定會停頓,且原本應該以如何實作設計為重點,結果變成在做設計的工作。即使「實作人員」和「設計師」角色由同一人扮演,情況還是很糟糕;更遑論是不同人,情況更是無法收拾。預防這種失控情勢就是測試優先設計提高效率的另一種方法。

  3. 測試設計對於「實作人員」的好處還有其他方面 -- 闡明設計。如果實作人員對於設計仍然存有疑問,則測試設計可做為期望行為的特定範例。如此,可望減少因實作人員誤解而發生的錯誤。

  4. 即使問題未曾(但應該要有)存在「實作人員」心中,仍然只有少數錯誤。 例如,「設計師」和「實作人員」的認知可能在無意中出現分岐的現象。如果實作人員遵循設計,也遵循元件預期用途的特定指示(從測試案例),則元件會更可能達到實際的需求。

範例

這裡有一些範例讓您體驗一下測試優先設計。

假設您要建立一個系統來取代以往指定會議室時的「詢問秘書」方法。 MeetingDatabase 類別的其中一個方法是 getMeeting,具有下列標記:

  Meeting getMeeting(Person, Time);

給定一個人名和一個時間,getMeeting 會傳回此人已安排在此時間參加的會議。如果此人沒有任何排程,則傳回特殊的 Meeting 物件 unscheduled。以下有幾個簡單的測試案例:

  • 此人在給定的時間不在任何會議上。傳回 unscheduled 會議嗎?
  • 此人在此時間正在參加會議。此方法傳回正確的會議嗎?

這些測試案例很普通,但最後一定要試驗。也可以現在就建立,撰寫有朝一日會執行的實際測試碼。第一個測試的 Java 程式碼可能類似:

      // 如果在給定的時間不在會議上,// 則預期為 unscheduled。    public void testWhenAvailable() { Person fred = new Person("fred"); Time now = Time.now(); MeetingDatabase db = new MeetingDatabase(); expect(db.getMeeting(fred, now) == Meeting.unscheduled);     }

還有一些更有趣的測試構想。例如,此方法會搜尋相符項目。每當方法執行搜尋時,可以試著想像如果找到多個相符項目會發生什麼事。在此情況下,表示詢問「一個人可能同時出現在兩個會議上嗎?」似乎不可能,但如果向秘書詢問這種情形,可能會有意想不到的答案。事實證明有些主管經常安排一次參加兩個會議。他們的角色是趕著走進會議,以最短的時間「整理部隊」,然後就起身離開。未顧及這種情況的系統,至少會有部分功能閒置。

這是在實作層次上擷取分析問題的測試優先設計範例。有幾項要點必須注意:

  1. 您一定希望理想的使用案例定義和分析可以發現這項需求。在此情況下,將可避免問題「逆流而上」,且 getMeeting 將以不同方式設計 (不能只傳回一個會議;必須傳回一組會議)。但分析總免不了遺漏一些問題,最好在實作期間就發現這些問題,在部署之後才發現就為時已晚。

  2. 在許多情況下,設計師和實作人員沒有足夠的領域知識可發現這些問題 - 他們沒有機會或時間來拷問秘書。在此情況下,getMeeting 的測試設計人員會問「有可能傳回兩個會議嗎?」,然後稍做思考,就認為不可能。可知,測試優先設計不可能擷取到每一個問題,只不過問對問題可讓發現問題的機會大增。

  3. 實作期間運用的一些測試技術也適用於分析。測試優先設計也可以由分析師來做,但這不是本頁的主題。

第二個範例是一套加熱系統的狀態圖模型。

HVAC 狀態圖

圖 3:HVAC 狀態圖

一組測試將通過狀態圖中的所有弧線。其中一項測試可能以閒置的系統開始、注入 Too Hot 事件,在 Cooling/Running 狀態中造成系統失敗、清除失敗、投入另一個 Too Hot 事件,然後讓系統回到 Idle 狀態。因為沒有推演所有弧線,需要更多測試。這幾種測試可找出各種不同的實作問題。例如,經由通過每一條弧線,可以檢查實作是否遺漏任何弧線。有些事件先經過失敗的路徑,接著又經過順利完成的路徑,透過這一連串事件,可檢查錯誤處理程式碼能否清除不完整的結果,因為這些結果可能影響後續的計算 (如需測試狀態圖的詳細資訊,請參閱工作成果準則:狀態圖與活動圖的測試構想)。

最後一個範例取自設計模型的一部份。債權人和發票之間有關聯,任何給定的債權人可能有多張尚未支付的發票。

Creditor 與 Invoice 類別關聯圖

圖 4:Creditor 與 Invoice 類別之間的關聯

依此模型的測試會以債權人沒有發票、有一張發票及大量發票的情形來推演系統。測試人員也會質問一張發票是否可能需要和多位債權人建立關聯,或一張發票是否沒有債權人 (電腦系統打算取代的對象 -目前採用紙張作業的人- 可能以無債權人的發票來追蹤擱置工作)。果真如此,那就是「分析」中會發現的另一個問題。

誰負責測試優先設計?

測試優先設計可能由設計的作者或其他人完成。通常由作者來負責。優點在於可節省溝通時間。工作成果「設計師」和「測試設計師」不必互相解釋問題。再者,另外的「測試設計師」必須花時間來學好設計,但原來的「設計師」本來就會。最後,諸如此類的問題 - 例如「壓縮機在狀態 X 故障時怎麼辦?」 - 大多是軟體工作成果和測試設計期間一定會提出的問題,所以您也可以對同一個人問一次問題,然後請他以測試格式寫下答案。

然而有缺點。首先,在某種程度上,工作成果設計師很難察覺自己的錯誤。測試設計流程可發現其中部分盲點,但成效可能不如由另一個人來發現。問題的嚴重性因人而異,通常與設計師的經驗有關。

由同一個人負責軟體設計和測試設計還有另一項缺點,就是缺乏平行性。雖然將這些角色分配給不同的人會導致總工作量增加,但可能縮減預計的行事曆時間。如果人們渴望從設計邁向實作階段,則將時間浪費在測試設計上會折損士氣。更重要的一點,經常會為了往前繼續進行而草率了事。

有辦法在元件設計時完成所有測試設計嗎?

不可能。原因是並非所有決策都在設計時決定。在實作期間所做的決策,不適合由設計建立的測試來做測試。最典型的例子就是陣列的排序常式。有許多不同的排序演算法,各有利弊。在大陣列上,快速排序通常比插入排序更快,但在小陣列上就比較慢。所以,對於超過 15 個元素的陣列,排序演算法的實作可能採取快速排序,否則就採取插入排序。從設計工作成果中,可能不易看出這種分工情形。這可以在設計工作成果中描述,但設計師可能認為做這種明確決策並不值得。因為陣列大小在設計中不重要,測試設計可能不慎只使用小陣列,這表示完全沒有機會測試「快速排序」程式碼。

再舉一例子,請看下列序列圖片段。其中顯示 SecurityManager 呼叫 StableStorelog() 方法。在此範例中,log() 傳回失敗,進而造成 SecurityManager 呼叫 Connection.close()

安全管理程式序列圖實例

圖 5:SecurityManager 序列圖實例

這對於實作人員有很好的提示作用。只要 log() 失敗,就必須關閉連線。測試要回答的問題是實作人員是否在全部案例或只在部分案例中這麼做(且做得正確)。為了回答這個問題,測試設計師必須找出所有 StableStore.log() 的呼叫,並模擬失敗狀況由每一個呼叫點來處理。

假設您已看過所有呼叫 StableStore.log() 的程式碼,執行這樣的測試似乎很奇怪。不能稍微看一下是否正確處理失敗嗎?

直接目視也許就夠了。然而,眾所皆知,錯誤處理程式碼向來就極易出錯,因為通常會隱含地依賴於錯誤本身就已破壞的假設。最典型的例子就是處理配置失敗的程式碼。範例如下:

  while (true) { // 最上層事件迴圈     try { XEvent xe = getEvent(); ...                      // 程式主體     } catch (OutOfMemoryError e) { emergencyRestart();     } }

這段程式碼試圖透過清理來回復記憶體不足的錯誤(釋出更多記憶體),然後繼續處理事件。假設這是可接受的設計。emergencyRestart 非常小心不要配置任何記憶體。問題是 emergencyRestart 會呼叫一些公用常式,這些常式轉而再呼叫其他公用常式,依此類推,最後會配置一個新的物件。除非已無記憶體而導致整個程式失敗。只透過目視,實在很難發現這種問題。

測試優先設計和 RUP 的階段

直至目前為止,我們都假設您努力儘早完成很多測試設計。亦即,您努力從設計工作成果中導出所有測試,後來只是根據實作內容來加入測試而已。但這可能不適用於「詳述階段」,因為如此完整的測試可能不符合反覆活動的目標。

假設正在建置一個架構原型,準備向投資人展示產品的可行性。可能以少數的關鍵使用案例實例為基礎。程式碼經過測試,可顯示是否支援這些實例。但是,如果建立更多測試,沒有壞處嗎? 例如,很顯然,原型可能忽視重要的錯誤狀況。為何不撰寫測試案例來記錄該錯誤處理的需求?

但如果原型顯示架構方法無法奏效,又會如何?結論就是放棄架構及其所有錯誤處理測試。在此情況下,設計測試所付出的努力將毫無價值可言。最好辦法就是等待,只設計必要的測試,檢查此概念實證原型是否真能證實此概念。

乍看之下,這似乎微不足道,實際上潛藏著極大的心理作用。「詳述階段」目的是解決重大風險。整個專案團隊應該以這些風險為重點。誤導成員們專注於微不足道的事,只會虛耗團隊的焦點和能量。

所以,測試優先設計在「詳述階段」中有何用武之地呢?在充分探查架構風險上,可扮演重要角色。考量團隊如何確實掌握風險是否已浮現或已避免,可讓設計流程更明確,甚至可以在最初就建立一套優秀的架構。

在「建構階段」,設計工作成果將形成最終型態。所有必要的使用案例實現化及所有類別的介面都會完成實作。因為階段目標是完成,適合採用完整的測試優先設計。後續事件應該無法通過少數測試(如果有的話)。

「初始」和「轉換」階段通常較不重視適合測試的設計活動。但時機成熟時,測試優先設計會派上用場。例如,在「初始」階段可用於候選的概念實證工作。以「建構」和「詳述」階段測試而言,則必須達成反覆活動的目標。