選單
GSS 技術部落格
在這個園地裡我們將從技術、專案管理、客戶對談面和大家分享我們多年的經驗,希望大家不管是喜歡或是有意見,都可以回饋給我們,讓我們有機會和大家對話並一起成長!
若有任何問題請來信:gss_crm@gss.com.tw
8 分鐘閱讀時間 (1577 個字)

為什麼程式需要單元測試? - 實作篇

Photo by Hello I'm Nik on Unsplash Photo by Hello I'm Nik on Unsplash

此篇文章主要帶大家簡單實作單元測試,有興趣就往下看吧!

大致流程

  1. 撰寫程式
  2. 補單元測試

若上述兩點相反,可能就是 TDD 的做法,這篇文章不會詳述該內容,有興趣可以自己再去了解。

PS. 不過這邊要強調一下,TDD 是開發方法論,而並非只是將單元測試提前撰寫(也就是說先寫單元測試並不能完全代表 TDD)。

撰寫程式

範例這邊是寫 Console App,但盡可能模擬 Web API 可能的行為實作。

SimulateService.cs

why unit test imp 3

SimulateController.cs

why unit test imp 2

Program.cs

why unit test imp 1

撰寫單元測試

建立測試專案

why unit test imp 5

MSTestxUnitNUnit 三種都可以建立,我個人比較推 NUnit,其它的沒什麼不好,只是像是 xUnit 就需要自己實作 Setup 和 Teardown,但 NUnit 本身就有提供方法可以處理,所以就蠻看個人需求。

另外記得字要看清楚,是要建立哪個框架(.NET Framework、.NET Core)!

why unit test imp 6

測試專案命名看個人喜好,讓人可以快速理解這是測試專案就是好命名!

why unit test imp 7

依照你的需求決定專案目標框架(TFM)。

why unit test imp 8

以上是 NUnit 測試專案建立後的初始樣子。(若不需要 Setup 方法,可以拿掉)

why unit test imp 9

對著類別方法右鍵可以執行偵錯測試。(當然你也可以用快捷鍵)

why unit test imp 10

Visual Studio → 檢視 → Test Explorer,可以看到圖形化介面。

why unit test imp 11

透過該介面也能執行偵錯測試並且看到一些你可能感興趣的訊息(ex. 錯誤訊息...)。

尋找測試目標

還記得前一篇有提到,如需在有限時間下寫出有價值的單元測試,就會需要做出抉擇,當然如果你全都要,我也不會阻止你。

那如果依據剛剛我們撰寫的那些程式來看,我可能會選擇先為 SimulateService 的 GetOutput 方法寫單元測試,為什麼呢?

why unit test imp 12

因為它裡面包含重要的業務邏輯(給 A 得 B、給非 A 得 C),相比之下 SimulateController 的 GetOutput 方法就沒那麼優先(沒那麼優先不是代表不用寫),因為它只有呼叫 SimulateService 做事情並無包含重要業務邏輯在裡面。

❗ 由上述描述可知,寫單元測試的重點在測試程式邏輯,所以有重要邏輯的地方,就會是我們優先選擇的地方!

那你可能會問 Program 呢?

  1. 靜態類別其實對單元測試本身並不是很友善,所以如果沒辦法排除這件事,對我來說就不會是優先考量對象。
  2. 它其實也只是呼叫 SimulateController 做事情並無包含重要業務邏輯在裡面。

撰寫測試

why unit test imp 15

其實你可以把單元測試想像成是在寫一些案例,比如我們已經知道 GetOutput 的重要業務邏輯,所以針對它寫出了兩個測試案例(GetOutputWithAInput、GetOutputWithBInput),測試案例命名我這邊是命名的比較隨意,建議是可以明確知道這個案例在測試什麼最好(ex. 包含欲測試方法名稱、是否成功、部分行為描述...等等)

額外補充

 既然重要業務邏輯測試完了,那順便也把 SimulateController 也測一測吧!

why unit test imp 13

一樣先找出測試目標(GetOutput),但值得注意的是這邊跟 SimulateService 情況有點不太一樣,雖然不包含重要業務邏輯,但它呼叫了 SimulateService 做事情。

❗ 首先要知道我關注的是 SimulateController 中的 GetOutput 方法,所以我一定需要先把 SimulateService 變成是我能夠控制的情況,為什麼呢?

因為我不希望 SimulateService 會實際影響我對 SimulateController 的測試,如果會相互影響,就不會是我們所謂的單元測試!

從實作上來看,它已經透過 IService 介面與 SimulateService 做出了隔離,這是單元測試樂見的,因為你可以更好的控制 SimulateController 本身。

❗ 那你一定會很好奇,什麼情況下 SimulateController 會控制不了?

why unit test imp 4

仔細思考一下上述實作,你覺得有辦法對 GetOutput 做單元測試嗎?答案是沒辦法!

因為你不管怎麼去測試它,一定都會被 SimulateService 影響到,這樣就失準了!

以程式設計的角度來看,我們可以認為 SimulateController 跟 SimulateService 有直接耦合的關係,耦合嚴重的程式碼幾乎寫不出單元測試!

那回過頭來看,我怎麼去測試 SimulateController 呢?

why unit test imp 16

why unit test imp 17

其實測起來跟 SimulateService 沒什麼太大的差別,但差別就在我們把 SimulateService 變成一個可以自己控制的物件,所以我們就可以來讓這個物件行為符合我們的預期,進而協助我們對 SimulateController 完成單元測試

❗ 這邊模擬假物件(Mock Object)是透過 Moq 來幫忙,大致上就是透過 IService 介面模擬一個我預期的 SimulateService 出來並在 SimulateController 建構時一併給它。

為什麼以前 .NET Framework 時期我們會需要用 Spring.net 或一些其它套件來幫忙處理相依性注入,而現在 .NET 就有預設 DI 容器可以幫我們處理,一方面是希望程式不要耦合那麼嚴重,另一方面可能就是為了可以方便測試,大家可以思考看看。

撰寫整合測試

既然 SimulateController 也都測試完了,那順便也把 Program 也測一測吧!

不過這邊就不是寫單元測試了,不是因為我懶得寫,而是考量 Console App 之所以在 Program 掛上 static 一定有什麼考量或怎麼樣,上面也有提到如果不能先排除靜態類別,那幾乎甭測,乾脆直接寫整合測試比較快。

why unit test imp 14

不過最近好像得了不 DI 一下,覺得渾身不對勁的病一樣,所以先用了比較劣化的版本來實作,正版可參考

why unit test imp 19

why unit test imp 20

其實測起來跟 SimulateService 沒什麼太大的差別,但之所以為什麼我還是覺得這是整合測試呢?

最主要還是透過了 System.IO 來幫忙我驗證結果,將 Console 輸出寫至 StringWriter,但當今天 StringWriter 輸出不如你預期時,是不是測試就失準了?如果是的話,我就會歸類在整合測試,因為對我來說有些是我無法控制的部分

總結

  1. 了解到單元測試可以先寫(TDD)或後寫,我個人比較傾向先寫,為什麼呢?
    • 先寫的幾項好處(可以抑制程式過於發散、初期涵蓋率高、以解耦的概念寫程式...等等),但先寫的水有點深,沒有一定經驗及把握,可能會花太多時間在這邊。
    • 後寫不是不好,我的建議會是先把當前程式寫一版測試出來(有可能會是介於單元和整合之間的測試程式),接著開始重構並把測試逐步轉為純單元測試,比起不寫,倒不如鼓起勇氣寫。
  2. 自己要把握出存在重要業務邏輯的地方,那個地方優先度就比較高。
  3. 觀察目標行為,透過一些技巧(模擬假物件...等等)實現單元測試。
  4. 分析並區別單元及整合測試。
  5. 解開程式耦合(盡可能符合 SRP、OCP、LSP、ISP、DIP)是單元測試的第一步

感謝各位把文看完,若有一些疑問,歡迎指教與補充!

這篇主要講實作,下篇會講怎麼把單元測試可視化(Visualization)。

dotnet部署至iis server
使用 Git Rebase Interactive 模式整理 Commit

相關文章

 

評論

尚無評論
已經注冊了? 這裡登入
Guest
2024/05/02, 週四

Captcha 圖像