[30天快速上手TDD][Day 3]動手寫 Unit Test

[30天快速上手TDD][Day 3]動手寫 Unit Test

前情提要

上一篇文章介紹了單元測試的 5W,這一篇則是要介紹 How,怎麼開始動手寫我們第一個 Unit Test。(終於可以寫程式了,笑...)

Day3 Unit testing 跨出第一步

本篇文章會以 Visual Studio 為開發工具,以 MSTest 為 Testing framework。

介紹如何從目標物件的方法,建立對應的單元測試。也會介紹如何從測試程式,來撰寫對應的目標物件。最後則會說明怎麼透過Visual Studio來觀看程式碼覆蓋率。

 

既有程式產生單元測試(VS2010)

首先,先建立一個 Library 專案,以最一般好懂的例子,裡面有一個 Calculator 的類別,一個 Add 的公開方法。程式碼如下所示:

        public int Add(int firstNumber, int secondNumber)
        {
            return firstNumber + secondNumber;
        }
  1. 在類別或方法內容中,按滑鼠右鍵,叫出選單。
  2. 點選「建立單元測試」的選項。
  3. VS2010 會跳出畫面,詢問你要針對哪一個目標類別,以及哪一些方法建立單元測試。(若在類別上,則預設所有方法會被勾選。若在某一個方法上,則只有該方法會被勾選)
  4. 可選擇將單元測試程式加入已經存在的測試專案,或由 VS2010 幫你自動建立一個測試專案。

Day3 建立單元測試step1

Day3 建立單元測試

 

接著 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 的結果。

所以,測試程式上有哪些東西呢?

  1. 測試程式中會先初始化一個目標物件,也就是 new 一個 Calculator,採預設的建構式。
  2. 並依據測試方法上的簽章,自動幫我們建立所需要的參數,連變數命名都是按照方法簽章上的定義來宣告。
  3. 若方法有回傳值,也會定義一個預期的回傳結果,變數命名為 expected,代表預期結果。
  4. 方法有回傳值,還會定義一個變數為 actual,為測試目標物件的實際回傳結果。
  5. 測試程式實際呼叫目標物件,欲測試的方法 (其實這個測試程式,即模擬外部如何使用目標物件)
  6. 驗證實際結果與預期結果,是否吻合。

很簡單吧,這邊不得不提,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。因此建議一定要熟記熱鍵,預設熱鍵組合如下:

  1. Ctrl+R, T: 執行單一測試
  2. Ctrl+R, A: 執行所有測試(開發時最常使用)
  3. Ctrl+R, Ctrl+T: 偵錯單一測試(測試失敗時,最常使用)
  4. Ctrl+R, Ctrl+A: 偵錯所有測試

Day3 執行測試

 

在測試結果視窗,就能看到各測試方法的結果,以及測試失敗的錯誤訊息跟 call stack。

Day3 測試結果

 

在撰寫單元測試的程式碼時,有個 3A 原則,來輔助設計測試程式,可以讓測試程式更好懂。3A 原則如下:

  1. Arrange : 初始化目標物件、相依物件、方法參數、預期結果,或是預期與相依物件的互動方式。
  2. Act : 呼叫目標物件的方法。
  3. 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 的方法。

Day3 產生方法

 

程式碼如下:

        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 開始撰寫,但這個用最簡單的方式滿足測試案例,有幾個好處:

  1. 當還沒有什麼 idea 時,至少可以先滿足第一個測試案例。(內心就會覺得有產出,跨出一步了)
  2. 一個紅燈一直在那,會壓抑自己過度設計的慾望。紅燈代表要馬上解決,這就是我們眼前的目標。
  3. 紅燈、綠燈、重構,會是一個讓開發人員很愉悅的節奏。就跟跳恰恰一樣美好。

 

測試覆蓋率

測試覆蓋率,或程式碼覆蓋率,指的是執行完測試程式後,所有 production code 被執行到的比率。相關詳細的介紹,請參考之前的文章:[測試]Code Coverage

當在 Visual Studio 中,建立測試專案後,方案底下會有個 Local.testsettings 檔,點開後選擇「資料和診斷」,啟用「程式碼涵蓋範圍」。如下圖所示:

Day3 設定涵蓋範圍 (1)

 

讀者可能以為這樣就可以看到 code coverage,但其實還有個小地方要設定。針對程式碼涵蓋範圍,double click,會跳出期望要算出 code coverage 的組件,選擇剛剛的 Library,選擇套用,這才設定完畢。如下圖所示:

Day3 code coverage setting

 

設定完執行一次全部的測試,在測試結果的視窗上,可以點選「顯示程式碼涵蓋範圍結果」,即可觀看程式碼涵蓋範圍,展開細節之後,可直接 double click 方法,即可移至該方法內容上。當有勾選要計算程式碼覆蓋率時,預設跑完測試後,程式碼就會被上色。有執行到的是淺藍色,沒被執行到的則是紅色。如圖所示:

Day3 code coverage color inline

 

視窗上有個按鈕,可以開關著色,如下圖所示:

Day3 顯示code coverage著色

 

小結

這篇文章其實很淺,目的是為了讓還沒動手寫過單元測試/測試程式的朋友們,可以 step by step 的動手玩玩看。

不過本篇文章提到的兩個切入點,都是後續文章或環節的重要起手式:

  1. 從既有程式碼產生單元測試,是重構既有程式碼很好用的一個方式。也是增加新的測試案例,很方便的方式。
  2. 從測試案例,產生對應的程式碼。這可以輔助我們,只開發需要開發的功能。Top-down 的設計方式,讓我們專注在解決眼前這個需求,也可以避免我們浪費很多不必要浪費的時間。

不管是哪一種角度當切入,設計物件時,有一個重要的原則,希望各位讀者用心記住:

設計物件,應思考外部如何使用這個物件,而不是 bottom-up 的思考,這個物件要提供哪些功能給外面用

其實,就像介面導向的原則一樣,只相依於介面,就可以只專注在抽象層面上,而不被實作細節所影響。


blog 與課程更新內容,請前往新站位置:http://tdd.best/