接著來討論一下單元測試究竟該怎麼寫,你可能也會有這樣的疑問,如果今天我的程式是要呼叫資料庫來取得資料,那是否真要接上一個資料庫去做單元測試呢?又公司的資料庫如果對外是連不到的,悲苦的工程師回家怎麼做單元測試呢? 那如果是外部API呢?網路不通時單元測試是不是就都壞掉了? 那該怎麼知道現在單元測試是真正的邏輯錯誤還是只是因為網路或是資料庫連不上導致的錯誤?是不是寫了單元測試後反而我要花大量的時間常常在檢查到底現在是錯哪邊啊…..。
其實我剛開始接觸單元測試的時候上面的問題也都想過一輪,但這也點出了單元測試一個很重要的特性
單元測試應該是隨時隨地都要能正確執行,只要它本身的邏輯是正確的!!
看完這句話你可能會默默響起OS,「阿鬼,你還是說中文吧~」。不過這段容我之後另外篇幅再做詳盡的解說,現在只要有這樣的觀念就好,單元測試不應該隨著你在的環境不同而有結果的落差,他關注的是邏輯而不是與外部的關聯。
**
**
那好,假設我們做到了單元測試跟外部的關聯都斷開了,只專注在自己的邏輯上,這樣就稱得上是好的單元測試了嗎? 當然不是! 單元測試的命名也是一個很重要的課題
單元測試的標題需要具備好的可讀性、明確、標題與測試的內容精確吻合
還記得上一篇文章提到,撰寫良好的單元測試應該像是規格書一般,不僅要讓專案品質提高,更是要充當新接觸專案人員能透過閱讀單元測試對程式有基本認識的工具。
舉例來說:有一個單元測試標題這樣寫「public void GetTest_呼叫得到True()」,對於一個不看單元測試內容的人來說,這個標題一點意義都沒有,首先他不知道這個方法裡面是在幹嘛的,再來他究竟會做什麼事情導致他得到True,帶入的參數意義是什麼…等,這樣的單元測試反而是造成專案難以維護的幫兇。
所以比較好的單元測試標題應該詳盡,例:「public void GetTest_帶入會員ID_應回該ID搜尋到的會員資料DTO」,盡量符合
受測方法_傳入參數意義_期望得到的結果
比起第一種命名方式是否明確易懂多了。
你可能又會冒出一個問題,如果這個方法帶進的ID查不到會員時,我會回傳Null,但怎麼在一個單元測試表示?這邊點出另一個重點
一個測試只應該關注一件事情,如果受測目標有多種狀況,應該分成好幾個測試去涵蓋所有邏輯
順著上面的邏輯,這個方法應該就會有另一個測試為「public void GetTest_帶入會員ID_如搜尋不到該ID的會員_回傳Null」。
所以如果一個方法裡面的IF ELSE很多,導致程式邏輯複雜度提高,則單元測試可能就會有對應的很多方法來涵蓋所有可能,所以如果有寫單元測試,則職責分離就是一個重要的課題,如果你把很多職責都放在同一段程式中,你的單元測試可能是倍數成長之外,測試也會變得很難撰寫。
從上面的舉例也可以看出,每個單元測試都只關注一種邏輯,一個方法難免包含多種邏輯狀況,但當修改程式時如果單元測試錯誤,也能幫助你快速鎖定可能是哪一段邏輯錯了,減少除錯的時間。再者,因為每個單元測試名稱都很明確,執行的方法帶入的參數也都明確的情況下,讓人閱讀時可以很容易進入狀況,符合一開始提到的可以執行的規格書,對專案是非常有幫助的。
接著來探討單元測試的內文該如何撰寫,首先應該符合所謂的3A原則
Arrange = 準備受測物件、參數、預期結果
Act = 執行受測方法
Assert = 驗證執行結果與預測結果是否一致
拿昨天的單元測試來說明
1 | [ ] |
Arrange中Sut(System Under Test ),受測的目標為EasyMethod這跟Class,而這個單元測試預期的結果為7。
Act中actual是EasyMethod執行Method1這個方法得到的結果。
Assert中用MsTest提供的Assert.AreEqual方法驗證得到的結果與預期的結果是否一致。
如果每個單元測試都照著這樣的風格撰寫,則對於閱讀的人來說會很清楚歸類出每個區段各自的職責。
那今天就談到這邊,之後應該就會有比較多的實作了(擦汗)