[測試]單元測試的意義

[測試]單元測試的意義

前言
相信大家多多少少都有撰寫過Unit Test的程式,當然在軟體開發的過程中,可能因為時程或其他外在因素而導致無法持之以恆。但套句Ruddy老師的話,『要相信雲端的程式,還是Local端的程式?都不是,應該要相信測試過的程式』。

一個程式怎麼樣才算完成可以交付,怎麼證明這個程式沒有問題,應該就要有一份測試程式來證明,這些程式在這些test case裡面,程式是沒有問題的。

在Martin Fowler的Refactoring Improving the Design of Existing Code 第一章裡提到,The First Step in Refactoring:

Whenever I do refactoring, the first step is always the same. I need to build a solid set of tests for that section of code. The tests are essential because even though I follow refactorings structured to avoid most of the opportunities for introducing bugs, I'm still human and still make mistakes.


重構是維持系統可維護性幾近於無可避免的動作,想要讓系統更乾淨、更有效率、更好維護,第一件事仍然是撰寫好測試程式,因為原本程式是可以正常運作,為了效率更好、更容易維護而導致程式結果錯誤,這是不被允許的。

撰寫可自動化的測試程式,是一個貫穿整個軟體開發過程的活動,從還沒開始撰寫實際程式,到未來維護、改版時都仍須倚重於這些測試程式。

其實在前面重構系列的文章中,有一篇就提到了Stub的用法與原由,請參考:[ASP.NET]重構之路系列v5 –單元測試, Just Do It!!,這一篇則是由單元測試為切入點,來說明單元測試的一些重要資訊。

單元測試的意義
單元測試的意義,是希望每一個測試的method,都有相當簡單明確的意義,就是要證明某一項功能在某一個case底下,程式是如預期一般運作的。

為什麼需要單元測試
因為整合測試有這幾個缺點,

  1. 整合測試無法快速的定位出錯誤點:
    整合測試為黑箱測試,當測試失敗時,只知道其中的功能有錯,而無法快速準確的定位是哪一個『單位』錯了。就像下圖一樣,中間有好多的路徑都可能會出錯。
    整合測試的路徑
     
  2. 整合測試花費的時間太久,需要的測試環境太複雜:
    正因為整合測試要跑得功能太多,環境也可能太複雜(需要外界的檔案、DataBase、服務等等...),所以需要花很多時間。對工程師來說,即使寫好測試程式,按一下測試要等2~5分鐘,是讓人相當氣餒的。久而久之,越跑越久,久到乾脆不跑了。
  3. 整合測試涵蓋率不足:
    即使我們使用了測試涵蓋率的工具,知道了哪些程式被整合測試測過,哪些沒被測過。我們卻不容易從整合測試的切入點,來補足沒有被涵蓋到的點。(雷公在雲上,要打到特定的人也是很有可能打歪的...)

 

以上的問題,都可以透過單元測試來解決。(前提是單元測試要寫對)
[註]基本上一個單元測試的執行時間如果超過0.1秒,就是一個很緩慢的單元測試程式。

何謂整合測試
簡單的說,就是與外部服務有相依的測試,我們稱之為整合測試。

何謂外部服務,例如以下幾種:

  1. 需連到資料庫。
  2. 需使用到網路。
  3. 程式需進行檔案存取(IO)。
  4. 需對測試環境進行特別的動作(例如需要先編輯設定檔,才能執行測試)。

 

單元測試應具備的特性
簡單用一個縮寫來表示:FIRST (參考自代碼整潔之道

  • Fast:快速。
  • Independent:獨立。
  • Repeatable:可重複。
  • Self-Validating:可反應驗證結果。單元測試不論成功或失敗,都應該要從測試的reporting直接瞭解其意義或失敗原因。
  • Timely:及時。單元測試應該恰好在使其通過的production code之前撰寫。

 

如何撰寫單元測試,而非整合測試
斬斷與外界服務的直接相依性,怎麼斬斷?透過介面是一個最好的方式。所有與外界服務(包括類別),都應該相依於介面,而不是直接相依於物件。(也就是IoC的方式,IoC的範例可以參考:

ASP.NET]重構之路系列v4 – 簡單使用interface之『你也會IoC』

這邊舉一個發票資料更新的例子 (圖片來源:Working Effectively with Legacy Code):

  1. 直接相依於DB class:
    dependency
     
  2. 透過介面後,讓InvoiceUpdateResponder Class相依於IDBConnection,在production code中,再將DBConnection Class注入。
    IoC

 

如此一來,在測試InvoiceUpdateResponder裡面的update()方法時,就不會與實際的DataBase相依。我們不再需要連接DataBase才能得到資料,而可以透過Stub Object的方式,直接定義該IDBConnection.getInvoices()回傳的資料清單。

何謂Stub
Stub指的是一種run time時建立的拋棄式instance,可以定義該Stub是繼承/實作哪一個介面或抽象類別,並指定哪一個方法該回傳什麼值(透過overrides方法),進而使得單元測試中的測試目標,不需與外界服務相依,而只需相依於介面,並將Stub Object注入。

實際的範例,請參考:[ASP.NET]重構之路系列v5 –單元測試, Just Do It!!裡面的步驟五,即透過Rhino.Mocks產生Stub:

  1. 定義這個stub物件是繼承/實作哪一個class(需要是abstract class或interface)。
  2. 定義被呼叫哪一個方法。
  3. 傳入哪一個參數。
  4. 預計會回傳什麼值。

 

結論
透過整篇文章,您應該可以確定一下,自己寫的測試程式,是屬於整合測試,或是單元測試。

強烈建議,整合測試專案與單元測試專案要拆開來放,當在開發階段或在CI server上建立Auto Build的時候,每一次程式碼的改變(開發告一段落或簽入至版本庫),都需要執行一次完整的單元測試,才能確保這一次的版本是如同預期,且沒有影響到其他程式。

如果整合測試專案與單元測試專案綁在一起,那這個動作就會有上述整合測試的缺點:慢!

慢,就代表不好用。不好用就代表大家不想用。大家不想用就代表導入障礙提高。最後花的建置成本可能就會付諸流水。

如果您還沒開始使用單元測試,建議您跨出第一步,會感受到新奇、興奮以及相當的挫折感。挫折感的來源,來自於production code耦合性過高,可測試性低,代表品質不好。

程式不能測試=品質不好?
這麼說雖沒有錯,但不夠完整。

程式的可測試性高,代表程式的耦合度低,耦合度低,則代表程式品質『可能』有一定水準。但,程式的可測試性低,沒法子測試,或難以測試,難以『維護』測試,則代表程式耦合度高,或是程式內聚力低,則代表程式品質不佳。這是全然無誤的。

所以,程式的可測試性,是系統的重要品質指標之一。

Reference

  1. 代碼整潔之道
  2. Working Effectively with Legacy Code

或許您會對下列培訓課程感興趣:

  1. 2019/7/27(六)~2019/7/28(日):演化式設計:測試驅動開發與持續重構 第六梯次(台北)
  2. 2019/8/16(五)~2019/8/18(日):【C#進階設計-從重構學會高易用性與高彈性API設計】第二梯次(台北)
  3. 2019/9/21(六)~2019/9/22(日):Clean Coder:DI 與 AOP 進階實戰 第二梯次(台北)
  4. 2019/10/19(六):【針對遺留代碼加入單元測試的藝術】第七梯次(台北)
  5. 2019/10/20(日):【極速開發】第八梯次(台北)

想收到第一手公開培訓課程資訊,或想詢問企業內訓、顧問、教練、諮詢服務的,請洽 Facebook 粉絲專頁:91敏捷開發之路