【單元測試的藝術】Chap 8: 好的單元測試的支柱


目錄

  • PART I: 入門
    • Chap 1: 單元測試基礎
    • Chap 2: 第一個單元測試
  • PART II: 核心技術
    • Chap 3: 透過虛設常式解決依賴問題
    • Chap 4: 使用模擬物件驗證互動
    • Chap 5: 隔離(模擬)框架
    • Chap 6: 深入了解隔離框架
  • PART III: 測試程式碼
    • Chap 7: 測試階層和組織
    • Chap 8: 好的單元測試的支柱
    • Chap 9: 在組織中導入單元測試
    • Chap 10: 遺留程式碼
    • Chap 11: 設計與可測試性

前言

好的單元測試:

  • 可信賴
  • 可維護
  • 易讀

一、撰寫可信任的測試

原則和技術:

  1. 決定何時刪除或修改測試
    • 何時:
      • 產品 bug
      • 測試 bug
      • 語意或 API 有變更(功能不變)
      • 矛盾或無效的測試
    • 如果測試和產品程式沒有問題,會需要修改的原因
      • 重新命名或重構
      • 去除重複程式碼
  2. 避免測試中帶著邏輯
    • 不該 包含:
      • switch, if, else 語句
      • foreach, for, while 迴圈
  3. 一次只測試一個關注點
    • 一個關注點,是一個工作單元的最終結果:
      • 回傳值
      • 系統狀態的改變
      • 對第三方物件的互動
    • 否則容易導致
      • 難以命名
      • 可讀性降低
  4. 把單元測試和整合測試分開
    • 如上一章節所說的綠色安全區域,讓測試更容易執行
  5. 推行程式碼審查(確保覆蓋率)
    • 程式碼審查:兩個人坐在一起交談,現場查看或修改同一段程式碼
      • 並非追求 100% 程式覆蓋率
      • 覆蓋率低於 20% 代表缺少很多測試

二、撰寫可維護的測試

  1. 測試私有或保護的方法
    • 讓方法變成公開方法
    • 把方法抽取到新類別中
    • 把方法改成靜態方法
    • 把方法改為內部(internal)方法
  2. 去除重複程式碼
    • 使用輔助方法來去除重複程式碼:如工廠模式
    • 使用 [SetUp] 去除重複程式碼
  3. 具可維護性的設計來使用 Setup 方法:Setup 用起來很容易,但也易導致測試可讀性和可維護性下降
    • Setup 濫用
      • 初始化只在某些測試中用到的物件
      • 冗長難懂的 Setup 方法
      • 在 Setup 方法中準備假物件
    • 結論:停止使用 Setup 方法,多利用工廠和輔助方法。
  4. 實作隔離測試:
    • 強制的測試順序:必須依照特定順序執行
    • 存在呼叫其他測試的隱藏動作:測試呼叫其他測試
    • 共享狀態損壞:測試共享記憶體裡面的某個狀態,卻沒有重置
    • 外部共享狀態損壞:整合測試共享資源,卻沒有重置
  5. 避免對不同的關注點進行多次驗證
    • 使用參數化測試:[TestCase]
    • 使用 try-catch
  6. 物件比較:
    • 提高測試可維護性:建立一個用來比較的物件
    • 覆寫 ToString():
        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < this.fields.Length; i++)
            {
                sb.Append(this[i]);
                sb.Append(".");
            }
            return sb.ToString()
        }
      
  7. 避免過度指定
    • 測試對一個被測試物件的純內部狀態進行驗證
    • 測試中使用多個模擬物件
    • 測試在需要使用虛設常式物件時,使用模擬物件
    • 測試在不必要的情況下,指定順序或使用了精準的參數匹配器

三、撰寫可讀性高的測試

可讀性有以下幾個方面:

  1. 命名單元測試
    • 被測試方法名稱:
      • 測試情境:說明了使用測試的條件;「如果我用一個 null 值來呼叫方法 X,那麼它應該執行 Y」
      • 預期行為:基於目前的情境,方法應該產生的行為結果或回傳值或行為方式;「如果我用一個 null 呼叫方法 X,那麼它應該執行 Y」
    • 如:MethodUnderTest_Sceneario_Beharvior()
  2. 命名變數
    • 如:Assert.AreEqual(COULD_NOT_READ_FILE, result)
  3. 使用好的驗證資訊
    • 不要重複測試框架的控制命令輸出的資訊
    • 不要重複測試名稱裡面包含的資訊
    • 如果沒有什麼有用的資訊,就什麼也別說
    • 提供關於什麼應該發生和什麼沒有發生的資訊,如果可能的話,提供應該發生的時間點
  4. 把驗證和操作分開
    • Bad Assert
        [Test]
        public void BadAssertMessage()
        {
            // 一些程式碼
            Assert.AreEqual(COULD_NOT_READ_FILE, log.GetLineCount("abc.txt");
        }
      
    • Good Assert
        [Test]
        public void GoodAssertMessage()
        {
            // 一些程式碼
            int result = log.GetLineCount("abc.txt");
            Assert.AreEqual(COULD_NOT_READ_FILE, result);
        }
      

小結

優秀的單元測試三大支柱:可讀性、可維護性、可信任性。重點是測試要隨著被測試系統一同成長和變化

#Unit Test #單元測試的藝術







你可能感興趣的文章

ASP.NET Core Web API 入門教學 - 使用PUT更新資料

ASP.NET Core Web API 入門教學 - 使用PUT更新資料

【JS 大魔王 - 1】閉包 Closure 與 Scope 作用域

【JS 大魔王 - 1】閉包 Closure 與 Scope 作用域

JS 學習筆記 : Map

JS 學習筆記 : Map






留言討論