單元測試概論

文: Areca Chen

前言

在程式設計的領域中有許多種測試單元測試只是測試中的一種所以單元測試並不保證程式是完美的。但單元測試在所有測試當中是非常重要的一種。單元測試是一種由程式設計師自行測試的工作。單元測試所測試的是其所撰寫的程式碼單元是依據其所設想的方式執行而產出合於預期的結果。就是程式碼的正確性。

缺乏測試的程式對程式設計師產生的後果是一種惡性循環因為缺乏測試的程式碼容易產生許多臭蟲,程式設計師為了修改臭蟲而在沒有測試的保護下又引發更多的臭蟲,因為程式設計師忙於除蟲也就更沒有時間測試,如此惡性循環的結果下往往導致專案的崩潰。為避免這種惡性循環的產生最重要的就是程式碼必須有測試的安全網來保護。

XP(eXtreme Programming)(終極程式設計)是由少數的幾項簡單的規則及實務作法所架構而成。在這些規則及實務作法中『單元測試』扮演重要的角色。單元測試在XP中提供整體程式的一個安全網,在XP專案開發當中單元測試是隨程式碼發行,以此在後續的開發當中前面的每次發行都必須執行前面所有的單元測試並且都是100%通過單元測試尤其是XP是在沒有完整的規劃下就開始設計程式,同時為了應付開發期間或完成後客戶對於程式功能的變動,若沒有測試作為安全網,除了在程式開發當中可能因變動而導致臭蟲,同時在後續的維護上也會產生各種問題。

單元測試並不是新的技術,只是一般軟體開發團隊並未落實測試的機制,或曰「測試也無法找出所有的臭蟲」,因此既使有測試也只是聊備一格或僅依程式設計師之認知隨意測試而無制度化之測試。要讓測試落實在軟體開發過程當中測試必須是程式設計師本身體認到其重要性且容易,同時在實作測試上非常容易並能自動執行;甚至當測試是一種樂趣時,程式設計師便樂於測試,也因此專案開發的過程變成一種良性循環。

單元測試提升開發速度及品質

由於軟體系統的開發往往受到截止日期的限制,加上需求隨時變動。因此要在預定時間內交付符合品質的軟體產品往往成為程式設計師的夢魘。為了解決這個問題,XP提出了一套方法。而其中單元測試更是影響速度及品質的一個重要作法。讀者可能會認為寫程式的時間都不夠了如何還要程式設計師撰寫測試的程式碼。但是如果考慮後續為程式所做的維護及除蟲的工作,使用單元測試保護的程式相對可以節省更多的時間;同時程式設計師對其程式碼有更多的把握。那麼程式設計師更容易接受測試的觀念

測試可以節省開發時間的觀念我們可以從XP的作法來看。XP中強調的漸進式(increment)開發方式(或稱少量發行),每一次發行(release)的只有少量的程式碼,如此一來若有臭蟲大部分是存在於最後發行的部分,此時程式設計師對於程式碼的內容記憶猶新範圍也不大,必然更容易除蟲,所耗用於除蟲的時間也更少。更重要的是這些測試都是自動執行的因為測試碼都是隨程式碼發行,亦即所有的測試是逐次累積,每次發行都執行所有的單元測試。執行所有的測試除了可以測試新的程式碼是否有問題同時可以確保新的程式碼沒有破壞舊有的程式碼其次若測試無法自動執行,程式設計師必然耗費許多時間於測試而抗拒測試或者僅測試他所想要測試的部分而不是執行所有的測試。因此測試必須是自動化;而且是可以按一個鈕即執行所有的測試。如此大大提升程式撰寫及測的效率,而每次發行後的版本可以說都是最完整且沒有臭蟲的(至少是已測試的部分)

單元測試另一個重要的作用是讓重整(refactoring)得以實現。重整是一種在不改變原有功能的條件下讓程式碼更乾淨更具有彈性的技術(乾淨且彈性的程式碼具有可維護性及擴充性)。由於重整時需要變動程式碼的結構,因此變動程式碼時往往因而改變了內部的功能而不自知。此時若有單元測試的安全網的保護可以避免產生臭蟲,而讓重整時不用害怕因修改程式碼結構而無意中產生了臭蟲,因此重整更容易的實現。或許在未來重整的工具可以自動的保護程式,但目前而言仍然需要依賴單元測試來保護重整的工作不至於破壞程式碼。

XP實施共享程式碼的作法。團隊中每一個人都可以隨時修改或重整程式碼,而且這些變動的部分並不限於自己所撰寫的部分因此在變動這些程式碼時具有高度的風險,若沒有單元測試的保護我們無法知道當我們變動了原先不是我們所寫的程式碼是否會破壞原有的功能。因此只要我們對程式碼作了變動後立即執行單元測試便可以確認我們有沒有產生了臭蟲。

所以我們可以發現單元測試除了可以檢測我們程式碼的品質,同時是一張安全網,在整個開發過程當中當我們對程式碼作任何新增或變動時保護我們的程式碼不至於受到破壞,大大的提升程式碼的品質。同時有了單元測試的保護除蟲的時間縮短了,程式設計師勇於開發新的程式碼也提升了專案開發的速度。

單元測試是一種設計

單元測試最新的觀念就是:撰寫單元測試是一種設計的過程,尤其是當你先寫測試再撰寫程式碼(test first)XP的創始人Kent Beck曾說:「先寫測試的程式設計方式不是測試(Test-first coding isn't testing)」。這是甚麼意思?其實這句話的本意就是先寫測試是一種設計的過程。何以這麼說?因為我們一般在設計階段並不是考慮程式碼如何實作,我們考慮的是介面及功能(也就是這個物件應提供哪些功能於此同時若我們也考慮如何測試,我們考慮的是這些介面所提供的功能是甚麼(設計的議題)而不是這些功能是如何實作(演算法(algorithm)註釋1。所以先寫測試就是一種分析設計的過程。此外先寫測試的結果除了給我們一個良好的設計同時也生產出相關的測試碼。有了這些測試碼程式設計師可以更快速的撰寫程式,而且有了測試的保護程式設計師對於他們的工作更有信心。

而先寫測的方式固然不是在程式完成後一次撰寫測試,也不是一開始便把所有測試撰寫完成。而是寫一點點測試寫一點點程式,一點點測試,一點點程式。(test a little, code a little.)這種方式固然依據你的需求而定,但不管如何一點點的做是最好的。因為你每次處理的範圍都只是一點點,因此更容易處理也更容易聚焦相對的程式碼的品質也更好。

哪些程式碼需要單元測試

單元測試的對象是甚麼?在物件導向程式設計中我們針對的是物件的方法。那是否意味著所有的方法都要測試?XP認為任何可能產生破壞的程式碼都要測試(You need tests for every class— for everything that could possibly break.)當你把測試當成設計時還有甚麼是不用測試的?的確除了簡單的存取(access)內部資料的功能因為是很簡單不可能發生錯誤的情況,所有的功能都應該有測試或者說物件的所有行為(behavior)都應該測試。但是否有可能把每一個功能的所有情況都測試,事實上也不太可能。因為如此做耗費太多的時間與精力。但如何取捨,這就需要依賴程式設計師的經驗。程式設計師的經驗從何而來?主要是來自其它的測試如功能測試或驗收測試。當其它的測試發現臭蟲,你再修改前就先要加入單元測試來抓住這個臭蟲。這是一種累積經驗的方式。此外當你把測試當成設計時,你為物件設計的功能都是測的標的

此外物件的功能或行為是由許多細節所構成的,是否這些細節都需要測試?這就牽涉到單元測試是白箱測試或黑箱測試的問題。基本上對物件而言是白箱測試,因為單元測試是測試其內部的行為,對功能或行為而言則是黑箱測試,因為單元測試不測試其內部細節。也就是說單元測試是針對物件的功能或行為作測試。我們給定一些值或邊界條件以測試其產出的結果是否是預期的。至於這些功能或行為的內部細節實作部分則不在單元測試的範圍之內。此外當這個功能或行為的實作細節非常複雜有可能產生破壞,那麼便應該進一步分解成多個方法,再針對這些方法作測試。註釋2

單元測試的作法

前面說明了單元測試的重要性,現在我們來討論單元測試的作法。我們依循XP『先寫測試』的方式來實作。

我們舉例來說:假設我們設計了一個類別Calculator其中有一個方法是sum(a,b)sum是把a+b的結果輸出),在我們還沒實作sum方法之前,我們就先針對sum的功能先寫測試。在測試中我們先給定a,b兩個變數一個值,然後在測試中以sum(a,b)計算其結果看看是不是我們所預期的。

a=3;

b=5;

c=8;

assertequal(c,sum(a,b));  //c的值sum(a,b)時表示測試成功

當然在你還沒有實作sum方法之前這個測試執行一定會失敗的,而這就是所謂的「只有當一個失敗的測試時才寫新的程式碼(only writing new code when a test is failing)」。然後我們實作sum函數讓測試執行成功。此外在先寫測試時我們預期sum函數會加總各種資料型別;可能是相同型別或不同型別相加(此時我們是實作方法的多型(polymorphous)。所以我們針對不同的情況設計測試,因此正確的實作時必須滿足所有的情況這個測試才能100%執行成功。

結論

單元測試不只是測試,它同時提供程式設計師設計的一個安全觀點,更進一步提供程式設計師信心。同時也讓軟體整體有一個安全網,進而提供軟體的彈性,可以讓專案團隊隨時重整以便讓程式變的更乾淨。同時軟體的彈性也可以允許客戶隨時變更他們的需求,而不至於因新功能的修改或新增而導致程式的不穩定甚至破壞。因此可以提供客戶更好的服務及信賴度。

為了讓程式設計師樂於單元測試他們的程式,單元測試必須是很簡單而且可以自動執行以降低他們的負擔。使用依據各種語言撰寫不同的單元測試框架xUnit;如JavaJUnitDelphiDUnit(請至http://www.xprogramming.com/software.htm下載相關語言版本的單元測試框架)可以很輕易的將此測試框架整合至你的開發環境中協助你撰寫測試並自動的執行測試。目前有許多軟體開發環境也都內建單元測是的功能,如BorlandJBuilder便將Erich GammaKent Beck合寫的JUnit整合至其開發環境中,甚至提供精靈(wizard)以幫助建立單元測試。相信經由開發環境的整合,撰寫單元測是的趨勢是愈來愈明顯也愈來愈重要,而程式設計師也很樂於撰寫單元測試。

 

註釋1:這種方式稱為意圖導向的程式設計(programming by intention)。就好像是有人已經把最辛苦的方法寫好了,而你只是送出一個訊息而已。就是傳出訊息並檢查其產出的結果是不是我們所預期的。當我們考慮物件的輸入的訊息及輸出的結果等於在設計這個物件的功能。

註釋2若將一個方法分解成許多私有的方法會有一個問題。單元測試的框架無法呼叫宣告為私有(private)或保護(protected)方法測試框架無法測試這些方法。可能的方式是將其設為公開(public)。但這樣會導致資訊的公開,不符物件導向封裝(encapsulate)資訊的原則。另一種可能的解決方式是在受測類別中宣告一個公開的方法,在此方法中撰寫測試碼,再由測試框架呼叫這個方法。不過這方式將測試碼與原始程式碼混雜在一起,也不是一種很好的方式。