此篇文章主要帶大家簡單實作單元測試,有興趣就往下看吧!
大致流程
❗ 若上述兩點相反,可能就是 TDD 的做法,這篇文章不會詳述該內容,有興趣可以自己再去了解。
PS. 不過這邊要強調一下,TDD 是開發方法論,而並非只是將單元測試提前撰寫(也就是說先寫單元測試並不能完全代表 TDD)。
範例這邊是寫 Console App,但盡可能模擬 Web API 可能的行為實作。
MSTest、xUnit、NUnit 三種都可以建立,我個人比較推 NUnit,其它的沒什麼不好,只是像是 xUnit 就需要自己實作 Setup 和 Teardown,但 NUnit 本身就有提供方法可以處理,所以就蠻看個人需求。
❗ 另外記得字要看清楚,是要建立哪個框架(.NET Framework、.NET Core)!
測試專案命名看個人喜好,讓人可以快速理解這是測試專案就是好命名!
依照你的需求決定專案目標框架(TFM)。
以上是 NUnit 測試專案建立後的初始樣子。(若不需要 Setup 方法,可以拿掉)
對著類別、方法右鍵可以執行、偵錯測試。(當然你也可以用快捷鍵)
Visual Studio → 檢視 → Test Explorer,可以看到圖形化介面。
透過該介面也能執行、偵錯測試並且看到一些你可能感興趣的訊息(ex. 錯誤訊息...)。
還記得前一篇有提到,如需在有限時間下寫出有價值的單元測試,就會需要做出抉擇,當然如果你全都要,我也不會阻止你。
那如果依據剛剛我們撰寫的那些程式來看,我可能會選擇先為 SimulateService 的 GetOutput 方法寫單元測試,為什麼呢?
因為它裡面包含重要的業務邏輯(給 A 得 B、給非 A 得 C),相比之下 SimulateController 的 GetOutput 方法就沒那麼優先(沒那麼優先不是代表不用寫),因為它只有呼叫 SimulateService 做事情並無包含重要業務邏輯在裡面。
❗ 由上述描述可知,寫單元測試的重點在測試程式邏輯,所以有重要邏輯的地方,就會是我們優先選擇的地方!
那你可能會問 Program 呢?
其實你可以把單元測試想像成是在寫一些案例,比如我們已經知道 GetOutput 的重要業務邏輯,所以針對它寫出了兩個測試案例(GetOutputWithAInput、GetOutputWithBInput),測試案例命名我這邊是命名的比較隨意,建議是可以明確知道這個案例在測試什麼最好(ex. 包含欲測試方法名稱、是否成功、部分行為描述...等等)。
既然重要業務邏輯測試完了,那順便也把 SimulateController 也測一測吧!
一樣先找出測試目標(GetOutput),但值得注意的是這邊跟 SimulateService 情況有點不太一樣,雖然不包含重要業務邏輯,但它呼叫了 SimulateService 做事情。
❗ 首先要知道我關注的是 SimulateController 中的 GetOutput 方法,所以我一定需要先把 SimulateService 變成是我能夠控制的情況,為什麼呢?
因為我不希望 SimulateService 會實際影響我對 SimulateController 的測試,如果會相互影響,就不會是我們所謂的單元測試!
從實作上來看,它已經透過 IService 介面與 SimulateService 做出了隔離,這是單元測試樂見的,因為你可以更好的控制 SimulateController 本身。
❗ 那你一定會很好奇,什麼情況下 SimulateController 會控制不了?
仔細思考一下上述實作,你覺得有辦法對 GetOutput 做單元測試嗎?答案是沒辦法!
因為你不管怎麼去測試它,一定都會被 SimulateService 影響到,這樣就失準了!
以程式設計的角度來看,我們可以認為 SimulateController 跟 SimulateService 有直接耦合的關係,耦合嚴重的程式碼幾乎寫不出單元測試!
那回過頭來看,我怎麼去測試 SimulateController 呢?
其實測起來跟 SimulateService 沒什麼太大的差別,但差別就在我們把 SimulateService 變成一個可以自己控制的物件,所以我們就可以來讓這個物件行為符合我們的預期,進而協助我們對 SimulateController 完成單元測試。
❗ 這邊模擬假物件(Mock Object)是透過 Moq 來幫忙,大致上就是透過 IService 介面模擬一個我預期的 SimulateService 出來並在 SimulateController 建構時一併給它。
為什麼以前 .NET Framework 時期我們會需要用 Spring.net 或一些其它套件來幫忙處理相依性注入,而現在 .NET 就有預設 DI 容器可以幫我們處理,一方面是希望程式不要耦合那麼嚴重,另一方面可能就是為了可以方便測試,大家可以思考看看。
既然 SimulateController 也都測試完了,那順便也把 Program 也測一測吧!
不過這邊就不是寫單元測試了,不是因為我懶得寫,而是考量 Console App 之所以在 Program 掛上 static 一定有什麼考量或怎麼樣,上面也有提到如果不能先排除靜態類別,那幾乎甭測,乾脆直接寫整合測試比較快。
不過最近好像得了不 DI 一下,覺得渾身不對勁的病一樣,所以先用了比較劣化的版本來實作,正版可參考。
其實測起來跟 SimulateService 沒什麼太大的差別,但之所以為什麼我還是覺得這是整合測試呢?
最主要還是透過了 System.IO 來幫忙我驗證結果,將 Console 輸出寫至 StringWriter,但當今天 StringWriter 輸出不如你預期時,是不是測試就失準了?如果是的話,我就會歸類在整合測試,因為對我來說有些是我無法控制的部分。
感謝各位把文看完,若有一些疑問,歡迎指教與補充!
這篇主要講實作,下篇會講怎麼把單元測試可視化(Visualization)。