l

2021年3月10日 星期三

用合約彌補測試案例的不足(下):透過合約發現系統行為缺陷

March 10 14:28~16:05

【圖1】ezKanban領域模型(部分)


前情提要

上集〈用合約彌補測試案例的不足(上):撰寫合約〉提到兩個撰寫合約的例子,今天介紹透過撰寫合約發現ezKanban系統bug的實際案例。

***

commitCardInLane的測試案例

在講解透過合約找到系統行為bug之前先介紹ezKanban的Board bounded context的部分領域模型,如圖1所示。圖1包含兩個Aggregate,Workflow與Card:

  • Workflow是Aggregate Root,它包含多個Lane。
  • Lane有兩種,垂直的叫做Stage,水平的稱為Swimlane
  • Card是Aggregate Root,它代表看板上面的一張卡片。Card放在某個Lane上面,兩者之間的關係用CommittedCard表示。

Workflow身上有一個commitCardInLane方法,表示某張卡片被放入某個Lane,其介面如下:

public void commitCardInLane(CardId cardId, LaneId laneId, int order, String userId)
  • cardId:要放入的card Id
  • laneId:要放在哪個lane上面的lane id
  • order:順序
  • userId:執行該操作的使用者 id

ezKanban採用TDD方式開發,針對commitCardInLane已經有一個測試案例如下:

【圖2】commitCardInLane的單元測試


由於workflow.commitCardInLane是一個很簡單的方法,它的實作類似以下兩行程式:

Lane lane = getLaneById(cardCommitted.getLaneId()).get();
lane.addCommittedCard(cardCommitted.getCardId(), cardCommitted.getOrder());

因為卡片實際上是放在lane上面,因此還需要幫lane.addCommittedCard寫一個測試,如圖3所示。


【圖3】這個測試案例規範一個行為—卡片的順序等於它被加入lane的順序。

寫完這兩個測試案例,感覺就差不多了,程式跑起來也都沒什麼問題。

***

commitCardInLane被疏忽的行為

再看一次圖4的commitCardInLane介面:

【圖4】commitCardInLane介面


當ezKanban團隊與Teddy幫commitCardInLen撰寫合約的時候,首先寫出以下幾個沒爭議precondition (前置條件):

  • car id is not null
  • lane id is not null
  • order >= 0
  • user is is not null
  • 要加入的那個lane必須存在

接著想到,既然卡片是要放到某個lane裡面,因此還需要一條:

  • 要加入的card 不存在lane id所屬的那個lane裡面

上述precondition只規範了同一張卡片不能同時存在同一個lane裡面。寫完之後又想到另一個問題:「卡片可以同時存在不同的lane裡面嗎?」不行,一張卡片只能出現在一個lane裡面,所以剛剛那條precondition寫得不夠精確,要改成:

  • 要加入的card 不存在所有的lane裡面


最後的precondition如下圖所示:

【圖5】commitCardInLane的preconditions


加上圖5的preconditions之後,就可以確保在這些前置條件滿足的情況之下,加入一張卡片之後,它就一定會依據正確的順序被放入正確的lane裡面。

最後,補寫一個單元測試用來確認剛剛最後一條前置條件所規範的行為,參考圖6。

【圖6】補寫測試案例來記錄由合約所發現的系統行為

***

討論

看到這裡,有經驗的鄉民可能會說:

從TDD/Specification by Example(實例化規格)的角度,「要加入的card 不存在所有的lane裡面」的這一個系統行為也是可以被找出來啊,只要在實例化規格的時候列舉出這個例子不就好了?根本不需要寫合約也可以發現。

這樣講「理論上」是沒錯的,但實際上:

  • 在實例化規格「找例子」的時候,也是有優先順序之分。團隊通常會把與領域專家開會的寶貴時間拿來討論最重要的系統行為。像是commitCardInLane(把一張卡片放到lane裡面)這種有點重要又不是太重要的行為,能夠想出「卡片要成功放入lane」以及「卡片的順序要和放入的順序一致」這兩個例子,已經算及格了。在有限的時間與壓力之下,在想例子的當下不見得很容易就想到「一張卡片不能重複放在多個lane」這個例子。
  • 寫合約就不一樣,因為人只要講到「簽訂合約」,立刻會從「君子模式」切換到「小人模式」。針對合約內容,能多挑剔就多挑剔,除了保護自己不要上當以外,還要幫自己取得最大利益。因此,光是從method介面,就很容易可以想出很多系統行為的「邊界條件」。

寫合約還有一個好處,它不需要像測試案例一樣,為了讓測試案例跑起來,需要:

  • 準備測試資料
  • 執行待測程式
  • 比對測試資料

寫合約只需要……寫合約。就是用一段程式碼描述程式的行為即可,這段程式碼一開始甚至可以是空的,只需要有名稱即可。例如圖5的205行card_not_committed_to_any_lay()用來代表「一張卡片不能重複放在多個lane」的前置條件。先把合約的名稱(要描述的系統行為)想好,在撰寫合約的檢查內容即可。

***

工商服務

在套用領域驅動設計的時候,你的物件導向領域模型都生不出來嗎?針對領域模型的狀態正確性感到很棘手嗎?你想透過撰寫合約來增加領域模型的正確性嗎?

你需要補充基本的物件導向養分,歡迎參加2021年4月20日(週二)的【敏捷開發懶人包:物件導向技能】一日課程。

***

友藏內心獨白:合約就是用規格取代舉例。

沒有留言:

張貼留言