很多人在寫測試時,常會陷入該去測試結果還是測試細節的困擾裡。雖然我的測試寫得不算太多,但我還是提供一些看法與實務上的經驗供大家參考。

從生活中的實例來看

我們從一個生活上的例子來說明好了,以下我會把「你」當成主角,這樣代入感會強烈一點。

假設你每週三下班後都要固定參加某個聚會,而這個聚會的開始時間是晚上七點半;換句話說,不管你何時下班,你一定要在七點半前出現在會場。

用稍微正式一點的 User Story 寫法就會像這樣:

為了「下班去參加每週三七點半的聚會」
身為「固定成員」
我要在「七點半前出現在會場」

好了,現在你需要交通工具去會場,以你的經驗,你覺得公車是最快的方式。

(為了解釋方便,這裡我會特意忽略走路等其他瑣碎的行為所耗費的時間,請不要太在意。)

所以以下是你的測試過程:

  • 假定我晚上六點半下班
  • 假定公車是晚上六點四十分來
  • 我搭公車搭了四十分鐘
  • 我斷定自己在晚上七點半前到達會場

看起來很完美,連續幾週下來這個測試也沒發生什麼問題,直到某天你嚴重遲到。

追查這個測試失敗的原因,你發現公車時刻更改了,原本六點四十分的公車改到六點五十分;另外加上七點時某段路口塞車狀況突然變得很嚴重,使得原本四十分鐘的路程變成五十分鐘。這兩個因素使得你必須另外找尋替代的交通工具,例如捷運或計程車,想當然這個測試也不能再用了。

從上面的例子可以看到,搭公車的細節影響著你對這個規格的驗證,但實際上規格並沒有要求你一定要搭公車。搭公車本身上一種 Non-Feature ,是開發者為了滿足需求而加入的額外條件。事實上你搭任何一種交通工具都可以,只要它能在時間內帶你到目的地就行。

既然在這個需求中搭公車是不必要的細節,我們可以把測試改成這樣:

  • 假定我晚上六點半下班。
  • 我搭上車程一小時以內的交通工具。
  • 我斷定自己在晚上七點半前到達會場。

現在你就可以搭任何一種交通工具,只要它能滿足你的需求。換句話說,你的程式是依賴在交通工具這個介面上。

這麼一來就能導出一個觀念:測試案例應該只測試關注的事情,當測試粒度越粗,關注的事也該越抽象

讓測試相依於介面的好處在於重構時,我們不用再對細節去調整測試;你只需要對介面去調整期望的輸出就好,即便是你想測試異常狀況。

那麼細節不重要嗎?

你可能會想:「不對呀,我還是對車程一小時要怎麼達成這件事沒什麼概念。像剛剛公車時刻和公車車程就很容易驗證,只有一小時這個條件的話我很難驗證,所以還是要驗證細節吧?」

當然,這裡我們確實需要對特定交通工具的車程有驗證方法,但它不屬於顯性的需求,我們要另外對這類隱性需求來獨立出我們的模組。

剛剛提到我們可以有不同的交通工具來達成需求,但我們因為經驗只選擇了公車;這對應到軟體開發實務上的話,也就是因為時程關係,我們會先用某種當下可行的做法。

這時候我們要對實作交通工具這個介面的公車做行為上的驗證,目標是一小時內的車程,所以我們可以對公車寫出以下的測試:

  • 假定晚上六點四十分有一班 306 公車從甲地出發。
  • 假定它到達乙地的的時刻為晚上七點二十分。
  • 我搭上這班公車。
  • 我斷定我到達乙地花了四十分鐘。

因為我們關注的對象是公車,所以這時候公車時刻和哪班公車這些細節對我們來說就是必要的。

把實作拆開來獨立測試後,你就可以專注在這個實作應該有的行為;重構也不成問題,只要它最後符合我們需要的介面即可。而當你需要更換實作時,也不會因此對上一層的測試有太大的影響,不論你是不是用到測試替身 (不用替身時也許會有小調整,例如更換實作類別) 。

實務經驗

我們在實作 API 時,就是利用這樣的思維在寫測試的:我們會先定義好 API 要怎麼用,也就是外部會怎麼操作這個 API ,所有的測試也是針對這個行為而寫。在實作上呢,它實際是透過 controller 去呼叫 service 的方法,而 service 層也會呼叫很多 libraries 或 respositories 來做資料上的互動。

一開始我負責寫 API 的測試與實作,但 service 則交給同事開發。我們討論好它們兩者之間用什麼方法溝通,用什麼格式溝通之後,就各自進行 TDD 開發。這麼一來,我不必擔心 service 實作會影響我的開發,同事也只要專注在 service 的行為是不是符合業務邏輯的需求就可以。

結論

我在開發架構的中心思想一直是:越上層就越抽象來貼近需求,越下層就越具體以達成實作;每一層都應該清楚它是因為了什麼目的而存在,進而專注去實現這個目的。

我後來發現這個思想也可以套用在寫測試上面:不論是哪一層的測試都應該專注在驗證該層被賦予的責任上,不要跨層去看下一層的細節是怎麼做的;結果在測試中大量測試了這些細節,導致在修改程式時,還得花大量時間去修改測試。