[30天快速上手TDD][Day 3]動手寫 Unit Test
前情提要
上一篇文章介紹了單元測試的 5W,這一篇則是要介紹 How,怎麼開始動手寫我們第一個 Unit Test。(終於可以寫程式了,笑...)
本篇文章會以 Visual Studio 為開發工具,以 MSTest 為 Testing framework。
介紹如何從目標物件的方法,建立對應的單元測試。也會介紹如何從測試程式,來撰寫對應的目標物件。最後則會說明怎麼透過Visual Studio來觀看程式碼覆蓋率。
既有程式產生單元測試(VS2010)
首先,先建立一個 Library 專案,以最一般好懂的例子,裡面有一個 Calculator 的類別,一個 Add 的公開方法。程式碼如下所示:
public int Add(int firstNumber, int secondNumber)
{
return firstNumber + secondNumber;
}
- 在類別或方法內容中,按滑鼠右鍵,叫出選單。
- 點選「建立單元測試」的選項。
- VS2010 會跳出畫面,詢問你要針對哪一個目標類別,以及哪一些方法建立單元測試。(若在類別上,則預設所有方法會被勾選。若在某一個方法上,則只有該方法會被勾選)
- 可選擇將單元測試程式加入已經存在的測試專案,或由 VS2010 幫你自動建立一個測試專案。
接著 VS2010 會把畫面直接帶到測試專案,你的測試類別上。(如果測試類別已經存在,新的測試方法會 append 在最下面)程式碼如下所示:
/// <summary>
///Add 的測試
///</summary>
[TestMethod()]
public void AddTest()
{
Calculator target = new Calculator(); // TODO: 初始化為適當值
int firstNumber = 0; // TODO: 初始化為適當值
int secondNumber = 0; // TODO: 初始化為適當值
int expected = 0; // TODO: 初始化為適當值
int actual;
actual = target.Add(firstNumber, secondNumber);
Assert.AreEqual(expected, actual);
Assert.Inconclusive("驗證這個測試方法的正確性。");
}
可以看到上面的程式碼,貼心的 VS2010 已經幫我們把測試程式的殼都建好了。
我們剛剛選取要測試的方法,是 Calculator 類別的 Add 方法,而 Add 方法,需要兩個 int 的參數,並回傳一個 int 的結果。
所以,測試程式上有哪些東西呢?
- 測試程式中會先初始化一個目標物件,也就是 new 一個 Calculator,採預設的建構式。
- 並依據測試方法上的簽章,自動幫我們建立所需要的參數,連變數命名都是按照方法簽章上的定義來宣告。
- 若方法有回傳值,也會定義一個預期的回傳結果,變數命名為 expected,代表預期結果。
- 方法有回傳值,還會定義一個變數為 actual,為測試目標物件的實際回傳結果。
- 測試程式實際呼叫目標物件,欲測試的方法 (其實這個測試程式,即模擬外部如何使用目標物件)
- 驗證實際結果與預期結果,是否吻合。
很簡單吧,這邊不得不提,Visual Studio 更貼心的部分是幫你把需要改的部分,加上了 //TODO 註解,當設定好之後,別忘了把 todo 註解移除唷。而最後一行 Assert.Inconclusive() 則是 VS2010 在自動產生完測試程式後,替開發人員防呆用的。所以寫好測試程式後,執行測試前記得移除 Assert.Inconclusive() 這一行。
假設外部的使用情境(也就是測試案例),是傳入 1 與 2,並期望 Calculator 的 Add 方法回傳為 3,那測試程式碼如下所示:
[TestMethod()]
public void AddTest()
{
Calculator target = new Calculator();
int firstNumber = 1;
int secondNumber = 2;
int expected = 3;
int actual;
actual = target.Add(firstNumber, secondNumber);
Assert.AreEqual(expected, actual);
}
執行測試也很簡單,在測試方法上,滑鼠右鍵即有「執行測試」的選項。但因為執行測試很常使用,而且絕大部分執行的時機點,都不是在測試專案上,而是寫完任一段落的 production code。因此建議一定要熟記熱鍵,預設熱鍵組合如下:
Ctrl+R, T
: 執行單一測試Ctrl+R, A
: 執行所有測試(開發時最常使用)Ctrl+R, Ctrl+T
: 偵錯單一測試(測試失敗時,最常使用)Ctrl+R, Ctrl+A
: 偵錯所有測試
在測試結果視窗,就能看到各測試方法的結果,以及測試失敗的錯誤訊息跟 call stack。
在撰寫單元測試的程式碼時,有個 3A 原則,來輔助設計測試程式,可以讓測試程式更好懂。3A 原則如下:
- Arrange : 初始化目標物件、相依物件、方法參數、預期結果,或是預期與相依物件的互動方式。
- Act : 呼叫目標物件的方法。
- Assert : 驗證是否符合預期。
程式碼上只需要加上註解,可讀性就會提升一些,如下所示:
[TestMethod()]
public void Add_Input_First_1_Second_2_Return_3()
{
//arrange
Calculator target = new Calculator();
int firstNumber = 1;
int secondNumber = 2;
int expected = 3;
//act
int actual;
actual = target.Add(firstNumber, secondNumber);
//assert
Assert.AreEqual(expected, actual);
}
額外補充一下,要記得改的通常還有一個地方,就是測試方法的名稱。因為當測試失敗時,應該要能迅速的由測試方法名稱判定,是哪一個方法或哪一種情境下,目標物件行為不符合預期。
[註1]感覺可以直接產生對應的測試方法,很過癮吧!但這個功能在 VS2012 被移除了,其中一個原因應該也是希望開發人員是使用 TDD 的方式進行開發,而不是寫完程式才回過頭來補測試程式。
[註2]在 VS2010 中,可以針對非 public 的方法進行單元測試,VS2010 會透過 reflection 幫忙產生一個測試目標的 accessor 物件。不過這個功能在 VS2012 也移除了,其中一個原因應該是因為這樣的測試方式,並不符合物件設計原則。針對這一點,後續我會再用一篇文章來進行說明。
由測試方法產生目標物件行為
假設我們需要 Calculator 提供一個減法的功能,傳入 3 , 2,則回傳結果應為 1。測試程式如下:
[TestMethod()]
public void Minus_Input_First_3_Second_2_Return_1()
{
//arrange
Calculator target = new Calculator();
int firstNumber = 3;
int secondNumber = 2;
int expected = 1;
//act
int actual;
actual = target.Minus(firstNumber, secondNumber);
//assert
Assert.AreEqual(expected, actual);
}
這時因為 Calculator 類別上,並沒有 Minus 方法,所以會建置失敗。這時只需要透過滑鼠右鍵,或是在 Minus 上,選「產生」(預設熱鍵為 Ctrl + .
),Visual Studio 即會在 Calculator 上產生 Minus 的方法。
程式碼如下:
public int Minus(int firstNumber, int secondNumber)
{
throw new NotImplementedException();
}
沒錯,由測試程式所產生的 production code,也會依據測試程式所給予的變數名稱,來當作方法簽章。當然,這個產生程式的方式,不僅限於測試程式產生 production code,而是只要沒有這個類別或這個方法,就都可以透過產生的方式,來產生 class/interface/enum,或是 property/function。
這時建置已經可以成功,但執行測試時,肯定會跳紅燈。你問我為什麼?因為我還沒發功啊...預設產生的方法內容是 throw new NotImplementedException(); 所以執行測試時,就會接到這個 exception。
但請相信我,這是好事。到這步驟,您 TDD 的起手式已經完成,這是「紅燈 > 綠燈 > 重構」循環的第一步:紅燈。
接著,只需要撰寫 Minus 方法,讓這個紅燈可以變成綠燈即可。任何方式都可以,包括直接 return 1;,或許您會覺得我怎麼可能直接 return 1 呢?但 TDD 講究的是,滿足測試案例,即代表功能符合預期。
當需要滿足其他需求,請增加測試案例。
不斷的紅燈、綠燈、重構,與增加測試案例,就代表目標物件越來越符合外部需求,也代表品質在不同場景下,出錯機率越來越低。
而且不必再擔心重構時,把程式改壞了,因為每次修改,都有越來越多的測試案例保護,改完馬上就會知道有沒那個地方冒煙了...
當漸漸熟悉這樣的方式之後,就不需要每次都從 hard-code 開始撰寫,但這個用最簡單的方式滿足測試案例,有幾個好處:
- 當還沒有什麼 idea 時,至少可以先滿足第一個測試案例。(內心就會覺得有產出,跨出一步了)
- 一個紅燈一直在那,會壓抑自己過度設計的慾望。紅燈代表要馬上解決,這就是我們眼前的目標。
- 紅燈、綠燈、重構,會是一個讓開發人員很愉悅的節奏。就跟跳恰恰一樣美好。
測試覆蓋率
測試覆蓋率,或程式碼覆蓋率,指的是執行完測試程式後,所有 production code 被執行到的比率。相關詳細的介紹,請參考之前的文章:[測試]Code Coverage。
當在 Visual Studio 中,建立測試專案後,方案底下會有個 Local.testsettings 檔,點開後選擇「資料和診斷」,啟用「程式碼涵蓋範圍」。如下圖所示:
讀者可能以為這樣就可以看到 code coverage,但其實還有個小地方要設定。針對程式碼涵蓋範圍,double click,會跳出期望要算出 code coverage 的組件,選擇剛剛的 Library,選擇套用,這才設定完畢。如下圖所示:
設定完執行一次全部的測試,在測試結果的視窗上,可以點選「顯示程式碼涵蓋範圍結果」,即可觀看程式碼涵蓋範圍,展開細節之後,可直接 double click 方法,即可移至該方法內容上。當有勾選要計算程式碼覆蓋率時,預設跑完測試後,程式碼就會被上色。有執行到的是淺藍色,沒被執行到的則是紅色。如圖所示:
視窗上有個按鈕,可以開關著色,如下圖所示:
小結
這篇文章其實很淺,目的是為了讓還沒動手寫過單元測試/測試程式的朋友們,可以 step by step 的動手玩玩看。
不過本篇文章提到的兩個切入點,都是後續文章或環節的重要起手式:
- 從既有程式碼產生單元測試,是重構既有程式碼很好用的一個方式。也是增加新的測試案例,很方便的方式。
- 從測試案例,產生對應的程式碼。這可以輔助我們,只開發需要開發的功能。Top-down 的設計方式,讓我們專注在解決眼前這個需求,也可以避免我們浪費很多不必要浪費的時間。
不管是哪一種角度當切入,設計物件時,有一個重要的原則,希望各位讀者用心記住:
「設計物件,應思考外部如何使用這個物件,而不是 bottom-up 的思考,這個物件要提供哪些功能給外面用」
其實,就像介面導向的原則一樣,只相依於介面,就可以只專注在抽象層面上,而不被實作細節所影響。
blog 與課程更新內容,請前往新站位置:http://tdd.best/