March 30 18:15~19:23
測試術語
採用BDD開發軟體,首先你先寫一個失敗的驗收測試規範系統的行為與範圍,接著開發production code讓驗收測試通過。在寫production code的時候如果驗收測試無法清楚表達實作程式的行為,則可以進一步撰寫一個失敗的單元測試,然後再用最簡單、最直接、最無腦的方式來撰寫production code。等待單元測試通過之後,再拿出Refactoring(重構)這一把「小刀」,將剛剛快速撰寫的production code修剪一番,朝向clean code目標邁進;後者就是傳統上大家所理解的TDD。
無論是先寫一個失敗的驗收測試,或是失敗的單元測試,這個「待測物」(System Under Test;SUT)通常需要借助其他物件的幫忙來達成任務,例如資料存取物件(OAD)相依於資料庫,網路應用程式需要Socket物件來建立網路連線。這些相依物件稱為Depended-on component(DOC)或Collaborator。
一個SUT的所有DOC集合稱為這個SUT的Context。為了提高SUT的重複使用性與可測性,在設計上希望SUT不要自己管理Context,而是由外部將它所需要的Context傳入,這也就是Dependency Injection(DI)的作法。
以上名詞解釋完畢,可以回到BDD身上。開發人員撰寫這個「失敗的測試案例」,可以從兩個角度切入,分別是驗證狀態與驗證行為。
- State-based testing:測試程式提供SUT所需的Context,然後呼叫SUT,最後驗證SUT的狀態是否正確。例如,傳入5給判斷質數的函數,傳回結果為true,如果傳入8則結果應為false。又例如push一筆資料到一個stack物件,則stack的容量會加一。以上做法都是藉由驗證SUT的狀態來判斷SUT的實作是否正確,有種黑箱測試的味道。
- Behavior-based testing:測試程式根本不管SUT的執行結果與狀態是否正確,只關心SUT與DOC的互動是否有如預期。聽起來有點玄,但在某些時候卻必須做這種方式的驗證。例如在Observer設計模式中,當subject狀態改變會自動呼叫observer的update函數。如果想驗證的重點是Observer設計模式是否實作正確,則測試者根本不管也不關心subject的business logic到底是在做什麼東東,只要確定當它狀態改變的時候真的會呼叫一次observer的update函數就可以了。這種測試白箱的味道很重,因為測試者必須知道SUT實作的邏輯才有可能知道要如何驗證SUT如何與DOC互動。
***
測試替身
講了這麼多還是沒提到和BDD有什麼關係。當開發人員要讓失敗的測試案例通過的時候,這時候SUT與DOC很可能都還沒產生(因為production code還沒寫啊),如果一口氣要把SUT與DOC全部寫好讓測試通過,可能需要花很長的時間。因此為了讓SUT可以在比較短的時間通過測試,開發人員可以採用Test Double(測試替身)的技巧「欺騙騙」SUT,先專注於SUT的開發而暫緩DOC的實質內容,又可以讓測試案例通過。
因為測試方式有狀態測試與行為測試這兩種,所以測試替身也可以分成這兩大類。Dummy、Stub、Fake屬於狀態測試,而Spy和Mock則屬於行為測試。
Teddy習慣用Dummy、Stub或Fake來做狀態測試,較少用Spy或Mock,總覺得Mock Object Library,例如mockito這種工具用「設定期望值」的方式來做行為測試,程式碼不是那麼容易閱讀,過一陣子沒看還要想一下才知道在驗證什麼。另一方面因為mock object library具有「純屬虛構」的能力,有可能真正的production code行為已經改變但使用mock object library所撰寫的測試案例還是通過,這也是有點傷腦筋的地方。
▼用mockito測試Command pattern
但是mock object library也有它的好處,尤其在做BDD/TDD的時候它可以讓你比較快速的採用piece by piece的方式完成SUT而不用先把DOC用「老師傅手工打造」的方式手刻一個假的版本出來。對於行為有一定穩定度的程式,例如最常見的就是Observer設計模式,用mock object library會比手刻DOC簡單很多。
***
無論是使用哪種測試替身,程式碼終究還是寫給人看的。工具本身並無對錯,在合適的時機下使用合適的工具需要由人來判斷,很多時候如何判斷時機才是最難的點。
***
友藏內心獨白:Mock到最後連開發人員都被騙了。
延伸閱讀
沒有留言:
張貼留言