單元測試的藝術第一章心得

初步定義

維基百科上對於「單元測試」的定義為 :

「一個單元測試就是一段程式碼(通常是一個方法),這段程式呼叫了另一段程式碼,然後驗證某些假設的正確性。如果這些假設是錯誤的,單元測試就會失敗。一個單元可以是一個方法或函數。」

 

初步定義

維基百科上對於「單元測試」的定義為 :

「一個單元測試就是一段程式碼(通常是一個方法),這段程式呼叫了另一段程式碼,然後驗證某些假設的正確性。如果這些假設是錯誤的,單元測試就會失敗。一個單元可以是一個方法或函數。」

我們在開發產品的時候 , 會依照某一個假設或是邏輯去實作某一個方法.而單元測試就是我們另外撰寫一個或多個方法去檢查 Production Code 是否有按照我們期待的方式去執行.

被測試系統 System Under Test (SUT)

  • 被你的測試程式所測試的對象

「代表 System Under Test,有些人喜歡用 CUT(Class Under Test 或 Code Under Test)。在測試中,被測試的東西稱為 SUT。」

作者以前覺得單元測試的傳統定義 , 技術上是正確的. 但後來作者改變了想法. 認為單元代表的是系統中的「工作單元」或是一個「使用案例」(use case).

工作單元的定義如下

從呼叫系統的一個公開方法,到產生一個測試可見的最終結果,在此期間內這個系統所發生的行為統稱為一個工作單元。所謂一個可見的最終結果指的是,我們只需透過系統的公共 API 和行為就可以觀察到它,而不需透過系統內部狀態才能得知結果

工作單元這個概念意味著一個單元,它既可以小到只包含一個方法,也可以大到包括實現某個功能的多個類別與函數。

作者認為工作單元不是越小越好。如果你所建立的工作單元越大,它的最終結果對使用這 API 的使用者可見度就越高,測試其實會更容易維護

一個最終結果可以是下列其中一種形式

  • 被呼叫的公開方法回傳一個結果值(回傳非 void 函數)
  • 在呼叫方法的前後,系統可見的狀態或行為發生變化,這樣的變化不需要透過查詢私有狀態就能取得與判斷
    • 例如:被測試方法會去創造一個使用者帳戶 , 則被測試方法執行前後是否有額外新增一個使用者帳戶. 是我們可以觀察到的結果
    • 例如:被測試方法會去修改某物件的 public 屬性 , 則被測試方法執行前後是否有正確修改該物件的 public 屬性. 是我們可以觀察到的結果
    • 例如:被測試方法執行後 , 會導致另外一個方法執行的結果不同 , 則此變化也是我們可以觀察的結果.
  • 呼叫一個不受測試所控制的第三方系統
    • 例如:呼叫一個第三方 log 系統。這個系統不是你寫的,而且你也沒有它的原始碼。

也就是說 , 我們可以透過驗證下列事項 , 來判斷我們的單元測試是否成功或是失敗

  • 方法的回傳值是否正確
  • 被測試方法執行後 , 是否會發生可見的狀態或是行為改變.
  • 被測試方法呼叫某個方法的次數是否正確.
到目前為止單元測試的定義

「一個單元測試是一段程式呼叫一個工作單元,並驗證工作單元的一個具體最終結果。如果對這個最終結果的假設是錯誤的,那單元測試就失敗了。一個單元測試的範圍,可以小到一個方法,大到多個類別。」

優秀單元測試的特質

單元測試應該具備以下特質:

  • 它應該是自動化,而且可被重複執行的
  • 它應該容易被實現
  • 它到第二天應該還有存在意義(不是臨時性的)
  • 任何人都可以按個按鈕執行它
  • 它的執行速度應該很快
  • 它的執行結果應該一致
  • 它應該要能完全掌控被測試的單元
  • 它應該是完全被隔離的(獨立於其他測試)
  • 如果它的執行結果是失敗的,應該要很簡單清楚地呈現我們的期望為何,問題在哪

很多人把對軟體進行測試與單元測試的概念混為一談,要釐清這個誤解,你可以問自己以下幾個問題:

  • 我兩週前所寫的單元測試,今天還能正常執行並得到結果嗎?兩個月前的呢?兩年前的呢?
  • 我兩個月前所寫的單元測試,團隊中任一人都能正常執行並得到結果嗎?
  • 我能在幾分鐘內跑完單元測試嗎?
  • 我能一鍵執行所有我寫過的單元測試嗎?
  • 我能在幾分鐘內寫出一個基本的單元測試嗎?

如果以上任一題答案是「不能」,那可能你寫的其實是整合測試。


整合測試

整合測試的目標

Integration testing is executed by testers and tests integration between software modules. 整合測試是測試系統是否能夠正常的運作 (複數模組合併)

整合測試的定義

「整合測試是對一個工作單元進行測試,而這個測試對被測試的單元並沒有完全的控制,而是使用該單元一個或多個真實依賴的相依物件,例如時間、網路、資料庫、執行緒或亂數產生器等等。」

對被測試的單元並沒有完全的控制可能帶來的問題 :

  • 例如 : 在程式中使用目前時間的 DateTime.Now,那麼每次測試執行所取得的都是不同時間,也就是說你無法設定被測試方法執行的環境. 此會導致測試結果不穩定.

另外整合測試還有一個問題 , 測試的東西太多了

  • 當發現錯誤時 , 會無法立刻判斷是程式碼中哪一個部分導致這個錯誤產生.

總結來說

單元測試與整合測試想要測試的東西不同

  • 整合測試會實際使用真實的相依物件或資源 , 確定模組整合後 , 實際上真的能夠運作(至少運作結果是可以接受的).
  • 單元測試為了保證單元測試的結果高度穩定以確實的驗證方法是否有正確執行某個假設 , 會將被測試單元與其他相依物件隔離開來

單元測試跟整合測試都很重要 , 單元測試用來驗證鎖是否可以正常開關 , 但若沒有整合測試 , 可能會如下圖那樣 , 沒有檢查到系統實際上無法正常運作

最終版的單元測試定義

「一個單元測試是一段自動化的程式碼,這段程式會呼叫測試的工作單元,之後對這個單元的單一最終結果的某些假設或期望進行驗證。單元測試幾乎都是使用單元測試框架進行撰寫的。撰寫單元測試很容易,執行起來快速。單元測試可靠、易讀、並且很容易維護。只要產品程式碼不發生變化,單元測試的執行結果是穩定一致的。」

簡單的單元測試範例

假設有個類別叫做 SimpleParser 需要測試。這個類別有個方法叫 ParseAndSum: 輸入是由零個或多個逗號(,)分開的數字所組成的一個字串,如果這個字串不包含任何數字,回傳 0,如果只有單一數字則回傳該數 int 值,如包含多個數字,則將數字相加後回傳總和。

// 範例目前只能處理零個或是一個的狀況
public class SimpleParser
{
    public int ParseAndSum(string numbers)
    {
        if (numbers.Length == 0)
        {
            return 0;
        }
        if (!numbers.Contains(","))
        {
            return int.Parse(numbers);
        }
        else
        {
            throw new InvalidOperationException("I can only handle 0 or 1 numbers for now!");
        }
    }
}

TestReturnsZeroWhenEmptyString 會驗證 ParseAndSum 的回傳結果, 若不正確或是有例外發生會輸出錯誤訊息到 Console

// 透過一個簡單的方法來測試傳空字串給 SimpleParser.ParseAndSum 的時候是否會回傳 0
class SimpleParserTests
{
    public static void TestReturnsZeroWhenEmptyString()
    {
        try
        {
            SimpleParser p = new SimpleParser();
            int result = p.ParseAndSum(string.Empty);
            if (result != 0)
            {
                Console.WriteLine(@"***SimpleParserTests.TestReturnsZeroWhenEmptyString:"+ 
                "------ Parse and sum should have returned 0 on an empty string");
            }
            else
            {
                // 顧名思義,我希望 Print 出這句。
                Console.WriteLine("Print me some success man!");
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
        }
    }
}

控制台測試程式

public static void Main(string[] args)
{
    try
    {
        SimpleParserTests.TestReturnsZeroWhenEmptyString();
    }
    catch (Exception e)
    {
        Console.WriteLine(e);
    }
}

測試驅動開發 TDD

TDD 的步驟

  • 撰寫一個會失敗的測試,以證明產品中程式或功能的缺失
  • 撰寫符合測試預期的產品程式碼,以通過測試
  • 重構程式碼

重構時機可以再多個測試完成後才進行 , 也可以通過每個測試後進行. 重構能使我們的 code 更易讀和好維護 , 同時我們還可以透過之前完成的單元測試來保證這次重購 , 並沒有導致功能壞掉.

重構意味著在不改變功能為前提之下 , 修改程式碼 , 改變其可讀性、可維護性.

TDD 的三種核心技能

  • 僅僅做到先撰寫測試,並不能保證測試是可維護、可讀且可靠的。(目前這本書在講的)
  • 僅僅做到撰寫出可維護、可讀、可靠的測試,並不能保證你能獲得測試先行的各種好處。
  • 僅僅做到測試先行,且測試可讀、可維護、可靠,並不能保證你能產出一個設計完善的系統。

想要成功使用 TDD , 你需要這三種技能

  • 知道如何撰寫優秀的測試(目前這本書在講的)
  • 撰寫程式碼前先寫測試
  • 良好的測試設計
作者建議 : 不要一次補足上述的三種技能 , 循序漸進地一個一個來. 不然通常是放棄收尾…

總結

一個優秀的單元測試會有下列特質

  • 一段自動化的程式,它會呼叫另一個方法,然後驗證這方法或是該類別的邏輯行為與預期結果相同
  • 用一個自動化測試框架進行編寫
  • 容易撰寫
  • 執行快速
  • 能由開發團隊裡任何人重複執行且得到一樣的結果

Thank you!

You can find me on

若有謬誤 , 煩請告知 , 新手發帖請多包涵

:100:

 

:muscle:

 

:tada:

 

:sheep:

原始文章將記錄於此
https://github.com/s0920832252/The_Art_of_Unit_Testing