推薦這個blog:

Award


(ASP.NET 2010、2011、2012年)

其他資源

簡體中文blog

最新回應

[測試]單元測試的意義

前言
相信大家多多少少都有撰寫過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


點部落-In Joey

↑ Grab this Headline Animator


關連文章

[Security]Cross-Site Scripting(XSS)的簡介與預防

[Security]SQL injection的簡介與預防

[Tool]靜態程式碼分析-FxCop

命名-Glossary的建立

回應

  • # re: [測試]單元測試的意義 by 魚乾

    寫測試碼時,我也一直很猶豫範圍,寫的太多,花了很多時間
    原來都寫到整合測試的範圍,特別是連db部份,還要重置data,想到都懶了
    但如果只重在單元測試,確實能集中心力,測試也不會花太久,
    開發過程應該也比較重視這一塊邏輯的部份
    給你推一下

    2011/12/27 下午 11:03 | 回覆

  • # re: [測試]單元測試的意義 by 91

    to 魚乾 :

    謝謝您的回覆。

    分享一下在上個case我們team的作法,我們針對db的部分,在開發的過程中,還是會寫DAO function對應的測試,並且直接連到測試DB來進行測試。

    目的是為了在layer-architecture的架構中,就算只寫好了DAO的function,我們也應該馬上確保這個function可以work,且無run time error。

    如果,這個DAO是需要資料初始化才能作業的,也就是這一次可以正常運作,下一次可能就失敗,這種情況,當第一次測試成功後,我就會把標記的[Test Method]拿掉,但code跟註解會留著。

    好處是,不讓這種要資料初始化的整合測試,影響到整個測試專案正常運作,且開發完一個function都可以馬上做驗證,相對的也可以省下規劃資料初始化的effort。壞處是,沒法子做到迴歸測試。

    但,架構都有了,第一次的整合測試程式也有了,當出現了bug,我們可以快速的為該bug的test case馬上模擬一份資料進行測試。

    這是我在DAL這一層做測試的折衷辦法,給您參考一下。

    通常還可以搭配一些整合測試類型的Smoke Test,以及QA/QE所建立屬於商業流程的Scenario,確保DAL以及整個商業流程程式碼執行無誤。(例如針對重點流程設計Selenium的錄製腳本)
     

    2011/12/28 上午 12:15 | 回覆

登入後使用進階評論

Please add 2 and 8 and type the answer here: