【單元測試的藝術】Chap 3: 透過虛設常式解決依賴問題


目錄

  • 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: 設計與可測試性

前言

先分享一下到目前為止的心得與感想,我原本對於對於單元測試的了解程度:確實有在公司專案中實作過,但基於時間總是緊湊並沒有累積足夠的經驗,也說真的不知到底對不對、又好不好。

總體來說,原本的我對於單元測試的理解:一知半解

那目前看完前面兩個章節,我感受到自己已經有了質的飛躍!(飄~)主要針對思維跟細節的部分,旁敲側擊的思考,觸發不同角度的靈感。

閒話家常,風塵僕僕,我們要進入重點章節了!接下來,我們來聊聊「虛設常式(stub)」,走起~

alt text

P.S. 這章節真的很長、很難,但也非常充實,我寫完之後重複看了十次以上,每次都有不同收穫,有些事慢慢就會懂了。


一、虛設常式簡介

在前一章,我們撰寫了第一個單元測試,並嘗試了幾個特性。這一章節,我們將討論更接近實務的例子。例如:測試的物件依賴於另外一個你無法控制(或尚未實作)的物件,可能是 Web 服務、系統時間、執行緒等等。關鍵點在於,你無法決定相依物件的 回傳值 ,虛設常式就派上用場了。老樣子,先來定義虛設常式。

虛設常式的定義:「虛設常式(stub)是在系統中產生一個可控的 替代物件,來取代一個外部相依物件(或協作者)。」

你可以在測試程式中,透過虛設常式來避免必須直接相依物件所造成的問題。

在詳談之前,先前提概要,下一個章節會再展開討論 虛設常式(stub)模擬物件(mock) 以及 假物件(fake),它們很像,又有很重要的不同處,所以先在你心中埋顆種子,現在你只需要知道模擬物件跟虛設常式相當類似,但你會對模擬物件進行驗證,不會對虛設常式進行驗證。


二、找到 LogAn 中對檔案系統的依賴

在 LogAnalyzer 類別中,可以針對多種 log 檔案的副檔名,來設定特定的轉接器(adapter)進行處理。我們假設系統所支援的日誌檔案格式設定,是被存放在硬碟中的某個地方,IsValidLogFileName 的方法內容如下:

    public bool IsValidLogFileName(string filename)
    {
        // 讀取設定檔案
        // 如果支援該副檔名,回傳 true
    }

那這個 method 有什麼問題呢?你的方法直接依賴於檔案系統。這種設計將使得被測試的物件無法進行單元測試,只能透過整合測試來驗證。
alt text

這就是 抑制測試(test-inhibiting) 設計的本質:當程式碼依賴於某個外部資源,即使程式碼邏輯完全正確,這種依賴仍可能導致測試失敗。


如何讓測試 LogAnalyzer 更簡單?

有句話是這麼說的:「任何物件導向的問題,都可以透過增加一層中介層來解決。」但若中介層過多又會是另一個問題,可能會讓邏輯過於複雜,這邊先不繼續討論。

現在如果要測試 LogAnalyzer 類別,唯一的方式是先在檔案系統中建立一個設定檔,但你試圖避免直接依賴於檔案系統,希望能讓單元測試更獨立些,而不是透過整合測試來確認。那接下來我們來談談怎麼解除依賴。

解除依賴的具體模式:

  1. 找到被測試物件所使用的介面API
    在太空人的例子中,這個介面就是太空梭模擬器裡的控制台與螢幕。
  2. 把這個介面的底層實作替換成你能模擬掌控的東西。
    將太空梭模擬器裡各種螢幕、控制台、按鈕都連接到控制室。在控制室裡,測試工程師可以控制太空梭所有介面顯示的內容,以便測試太空人是否能正常作業。

將這樣的模式實作,需要更多的步驟:

  1. 找到導致被測試的工作單元無法順利測試的介面
    (在 LogAn 專案中,指的就是檔案系統中的設定檔。)
  2. 如果被測試的工作單元是直接相依於這個介面,可以透過在程式碼中加入中介層,來隱藏這個相依的行為。
    (在 LogAn 我們會加入一個中介層,其中一個方式是將直接讀取檔案系統的行為,移到一個單獨的類別中(FileExtensionManager)。)
  3. 將這個相依介面的底層實作內容替換成你可以控制的程式碼。
    (在 LogAn 我們將被測試方法所呼叫的執行個體(FileExtensionManager),替換成另一個虛設常式的類別(StubExtensionManager)。)

接著,來看這樣的想法怎麼透過重構(refactoring)來調整程式碼,並在設計中加入接縫(seam)的概念。


三、重構設計以提升程式碼的可測性

是時候介紹這本書中常會提到的兩個術語了:重構(refactoring)接縫(seam)

重構的定義:「重構是在不改變程式碼功能的前提下,修改程式碼的動作。也就是說,程式碼在修改前後的工作是一致的,不多也不少,只是程式碼看起來跟原本不一樣了。常見的重構:重新命名一個方法,或是把一個較長的方法內容拆成幾個較短的方法。」

  • 提醒:如果沒有任何一種自動測試保護下,就冒然重構,可能會造成你的職涯提早結束。

接縫的定義:「接縫是指在程式碼中可以抽換不同功能的方法,這些功能例如:使用虛設常式類別、增加一個建構函式(constructor)參數、增加一個可設定的公開屬性、把一個方法改成可供覆寫的虛擬方法,或是把一個委派拉出來變成一個參數或屬性,供類別外部來決定內容。接縫透過實作開放封閉原則(Open-Closed Principle)來完成,類別的功能開放擴充彈性,但不允許直接修改該功能內實作的原始程式碼(類別功能對擴充開啟,對直接修改封閉)。遵循開放封閉原則,設計的程式碼就會有接縫。」


你可以在重構程式碼過程中,透過加入一個新接縫,既可調整原本程式碼的設計,同時又可以維持原有的功能不被改變,這就是為什麼前面新增了 IExtensionManager 的介面。

在測試中要解除依賴,可以在程式碼加入一個或多個接縫,只要能確保重構後程式碼所提供的功能與重構前完全一樣。

解除依賴有兩種重構方式,其中後者相依於前者:

  • A 型:將具象類別(concrete class)抽象成介面(interfaces)委派(delegates)
    • (α1) 擷取介面以替換底層實作內容
  • B 型:重構程式碼,以便將委派介面的偽實作注入目標物件中。
    • (β1) 在被測試類別中注入一個虛設常式的實作內容
    • (β2) 在建構函式注入一個假物件
    • (β3) 從屬性的讀取或設定中注入一個假物件
    • (β4) 在方法被呼叫前注入一個假物件

接下來將逐一說明。


(α1) 擷取介面以便替換底層實作內容

α1.1: 擷取出讀取檔案系統的類別,並呼叫它的方法
    public bool IsValidLogFileName(string fileName)
    {
        FileExtensionManager mgr = new FileExtensionManager();
        return mgr.IsValid(fileName); // 使用被擷取出來的類別
    }

    class FileExtensionManager
    {
        public bool IsValid(string fileName)
        {
            // 在這裡讀取檔案
        }
    }

接著,可以增加一個「某種形式的 ExtensionManager」取代具體的 FileExtensionManager,可以透過一個基底類別或介面讓 FileExtensionManager 繼承或實作。

接下來,我們來調整一下程式碼,透過一個新介面來讓程式碼更容易測試。


α1.2: 從一個已知的類別擷取出介面
    public class FileExtensionManager: IExtensionManager // 實作這個介面
    {
        public bool IsValid(string fileName)
        {
            ...
        }
    }

    public interface IExtensionManager // 定義這個新介面
    {
        bool IsValid(string fileName);
    }

    public bool IsValidLogFileName(string fileName)
    {
        IExtension mgr = new FileExtensionManager(); // 定義這個介面型別的變數
        return mgr.IsValid(fileName);
    }

以上程式碼,你建立一個介面 IExtensionManager ,上面有個 IsValid(string) 的方法,並讓 FileExtensionManager 實作這個介面。

程式碼的功能沒有改變,但現在已經可以在測試程式裡面,用一個自己建立的「假的」manager 來取代原本「真的」 FileExtensionManager 以便獨立測試。

你還沒建立這個虛設常式的 ExtensionManager,下一步,我們來建一個吧!


(β1) 在被測試類別中注入一個虛設常式的實作內容

β1.1: 一個總是回傳 true 的簡單虛設常式程式碼
    public class AlwaysValidFakeExtensionManager: IExtensionManager // 實作 IExtensionManager 介面
    {
        pubic bool IsValid(string fileName)
        {
            return true;
        }
    }

首先,留意這個類別的命名,這個類別StubExtensionManagerMockExtensionManager,而是 FakeExtensionManagerFake 這個字眼,說明這個類別物件類似另一個物件,但它可能被當作「模擬物件(mock)」或「虛設常式(stub)」。(下一章會說明什麼是模擬物件)

透過 Fake 的命名,你就可以延遲決定,應該拿來當作模擬物件或虛設常式。

現在你有了一個介面,以及兩個實作此介面的類別,但目前被測試的方法還是呼叫具象類別:

    public bool IsValidLogFileName(string fileName)
    {
        IExtension mgr = new FileExtensionManager(); // 定義這個介面型別的變數
        return mgr.IsValid(fileName);
    }

接著,我們得想辦法讓被測試的方法能去呼叫假物件,而不是直接使用 IExtensionManager 原本的實作內容。因此,需要再程式碼的設計中加入一個 接縫,以便注入虛設常式進行模擬。


依賴注入:在被測試單元中注入一個假的實作內容

在好幾種可行的方式,讓你可以建立基於介面的接縫,這些接縫讓你可以在類別中注入實作這個介面的物件,讓原本與類別的互動,改使用介面的方法。

幾個注意的方式:

  • 在建構函式中得到一個介面的物件,並將其存到欄位(field)中供後續使用。
  • 在屬性 get 或 set 方法中得到一個介面的物件,並將其存到欄位中供後續使用。
  • 透過下列其中一種方式,在被測試方法呼叫前獲得一個介面的假物件:
    • 方法的參數(參數注入)
    • 工廠類別
    • 區域工廠方法(local factory method)
    • 前面幾種方式的變形

參數注入的方式相當簡單:給方法增加一個參數,就可以在呼叫這個方法時,傳入一個依賴物件進去。

接下來,將逐一介紹其他的依賴注入方式。


(β2) 從建構函式注入一個假物件(建構函式注入)

透過建構函式注入的過程
alt text

β2.1: 使用建構函式注入你的虛設常式
    public class LogAnalyzer // 定義產品程式碼
    {
        private IExtensionManager manager;
        public LogAnalyzer(IExtensionManager mgr) // 定義可被測試程式使用的建構函式
        {
            manager = mgr
        }
        public bool IsValidLogFileName(string fileName)
        {
            return manager.IsValid(fileName);
        }
    }

    public interface IExtensionManager
    {
        bool IsValid(string fileName);
    }

    [TestFixture]
    public class LogAnalyzerTests // 定義測試程式
    {
        [Test]
        public void IsValidFileName_NameSupportedExtension_ReturnsTrue()
        {
            FakeExtensionManager myFakeManager = new FakeExtensionManager(); // 準備一個回傳 true 的虛設常式物件
            myFakeManager.WillBeValid = true;

            LogAnalyzer log = new LogAnalyzer(myFakeManager); // 傳入虛設常式物件
            bool result = log.IsValidLogFileName("short.ext");

            Assert.True(result);
        }
    }

    internal class FakeExtensionManager: IExtensionManager // 建立一個最簡單的虛設常式內容
    {
        public bool WillBeValid = false;
        public bool IsValid(string fileName)
        {
            return WillBeValid;
        }
    }

你應該可以看到以上的假物件,和之前看到的不一樣,之前是類似這樣:

    public class AlwaysValidFakeExtensionManager: IExtensionManager // 實作 IExtensionManager 介面
    {
        pubic bool IsValid(string fileName)
        {
            return true;
        }
    }

在測試程式中,可以自由設定假物件方法被呼叫時,要回傳什麼值。也就是 FakeExtensionManager 的 WillBeValid,這樣代表這個虛設常式類別可以在多個測試案例中重複使用

另外要留意的是,透過建構函式中使用參數來注入,這設計使得這些參數成為了必要的依賴,這是設計上的選擇,將使得使用者必須為每個特定的依賴傳入參數。

  • 關於建構函式注入的警告
    透過建構函式注入假物件,可能會衍生問題。如果類別需要多個虛設常式,加入越來越多的建構函式就會越來越困難。
    例如 LogAnalyzer 除了原本的檔案類型管理器,還額外需要依賴 Web 和 Log 服務,那麼建構函式可能如下所示:

      public LogAnalyzer(IExtensionManager mgr, ILog logger, IWebService service)
      {
          manager = mgr;
          log = logger;
          svc = service;
      }
    

    解決這個問題的其中一個方式,就是建立一個特殊類別,用來裝載要初始化被測試類別所需的所有值。這也叫稱為參數物件重構(parameter object refactoring)。
    另一個可行方案是控制反轉(Inversion of Control, IoC)容器,利用特殊的工廠方法。可以到 這裡 了解更多。

  • 何時該使用建構函式注入
    作者的經驗是,除非使用控制反轉(IoC)容器框架來初始化物件,否則使用建構函式會讓測試程式看起來更笨拙,但是,但是但是但是但是,作者表示通常還是會選擇使用建構函式注入,因為在 API 的可讀性跟語意上,這方式所帶來的影響是最小的。


用假物件來模擬異常

來看一個簡單的範例,在這個範例會說明,如何透過設定假物件來拋出例外。

假設需求:當檔案類型管理器拋出一個例外,期望被測試類別應該回傳 false,而不是把例外直接往外拋。(實務中不建議這樣處理例外)

    [Test]
    public void IsValidFileName_ExtManagerThrowsException_ReturnsFalse()
    {
        FakeExtensionManager myFakeManager = new FakeExtensionManager();
        myFakeManager.WillThrow = new Exception("This is fake");

        LogAnalyzer log = new LogAnalyzer(myFakeManager);
        bool result = log.IsValidLogFileName("anything.anyextension");

        Assert.False(result);
    }

    internal class FakeExtensionManager: IExtensionManager
    {
        public bool WillBeValid = false;
        public Exception WillThrow = null;
        public bool IsValid(string fileName)
        {
            if (WillThrow != null)
            {
                throw WillThrow;
            }
            return WillBeValid;
        }
    }

為了讓這個測試通過,你必須在被測試方法中增加一個例外處理的 try-catch,在 catch 區塊中回傳 false。


(β3) 透過屬性 get 或 set 注入假物件

這個方式是為每一個相依的物件建立一個對應的 get 與 set 屬性,然後在測試過程中使用這些相依物件。

你的測試程式會與上述建構函式注入程式碼很類似(同樣都是使用依賴注入(Dependency Injection)的技術),但更好讀,更容易撰寫,因為每個測試可以根據需求來設定自己需要的屬性。

alt text

使用屬性進行依賴注入。這方式比建構函式注入更加簡單,因為每個測試可以根據需求來設定自己需要的屬性。

β3.1: 透過在被測試類別中,增加一個新屬性來注入假物件
    public class LogAnalyzer
    {
        private IExtensionManager manager;
        public LogAnalyzer()
        {
            manager = new FileExtensionManager();
        }
        public IExtensionManager ExtensionManager // 允許透過屬性來設定相依物件
        {
            get { return manager; }
            set { manager = value; }
        }
        public bool IsValidLogFileName(string fileName)
        {
            return manager.IsValid(fileName);
        }
    }

    [Test]
    public void IsValidName_SupportedExtension_ReturnsTrue()
    {
       // 設定虛設常式(stub),確保回傳 true,可以參照前面的作法,如:β2.1 [Test]
       ...

       // 建立 analyzer,依賴注入虛設常式(stub)
       LogAnalyzer log = new LogAnalyzer();
       log.ExtensionManager = someFakeManagerCreatedEarlier; // 注入虛設常式

       // Assert 邏輯
       ...
    }

就像建構函式注入一樣,屬性注入也定義了哪些相依物件是必須的,哪些是非必需的,這都會對 API 的設計造成影響。透過使用屬性來定義相依物件的方式,其實是在表達一件事:要使用這個類型的物件,這個相依並不一定非得存在不可。

  • 何時該使用屬性注入
    如果你想表達出對被測試類別來說,這個相依物件並非是必要的,或是在測試過程中這個相依物件會被建立預設的物件執行個體,進而避免造成測試問題。

(β4) 在呼叫方法之前才注入假物件

這一節所討論的場景是針對:當你要對某一個物件進行操作前,才獲得該物件的執行個體,而不是取用透過建構函式傳入的參數或屬性注入的物件。在前面幾個小節的討論中,假物件都是在測試開始進行之前,在被測試類別以外就設定好假物件,再透過建構函式或屬性注入。

  • A. 使用工廠類別
  • B. 在發布版本中隱藏接縫
  • C. 不同的中間層深度等級(淺到深)
  • D. 偽造方法——使用一個區域的工廠方法(擷取與覆寫)

A. 使用工廠類別

在這種情況下,回到了最基本的設計方式,在被測試類別的建構函式中,初始化管理執行個體,但這個執行個體來自工廠類別。

在測試程式中,將設定工廠別(在這個例子中,使用一個靜態方法回傳一個實作 IExtensionManager 的物件執行個體)回傳一個虛設常式物件,而不是實際產品程式碼中實作 IExtensionManager 的類別。

alt text

在測試程式中,因為設定了工廠類別的行為,因此測試過程可取得一個虛設常式物件。當在產品程式碼中,透過該工廠類別取得物件時,仍會回傳原本產品程式中所預期的物件,而非虛設常式物件。

β4.A.1: 在測試執行過程中,設定工廠類別回傳一個虛設常式物件
    public class LogAnalyzer
    {
        private IExtensionmanager manager;
        public LogAnalyzer()
        {
            manager = ExtensionManagerFactory.Create(); // 在產品程式中使用工廠類別
        }
        public bool IsValidLogFileName(string fileName)
        {
            return manager.IsValid(fileName) && Path.GetFileNameWithoutExtension(fileName).Length > 5;
        }
    }

    [Test]
    public void IsValidFileName_SupportedExtension_ReturnsTrue()
    {
        // 設定虛設常式(stub),確保回傳 true
        ...

        ExtensionManagerFactory.SetManager(myFakeManager); // 為這個測試設定虛設常式,並注入工廠類別

        // 建立 analyzer,依賴注入虛設常式(stub)
        LogAnalyzer log = new LogAnalyzer();

        // Assert 邏輯
        ...
    }

    class ExtensionManagerFactory
    {
        private IExtensionManager customManager = null;
        public IExtensionManager Create() // 調整工廠設計,使其能使用與回傳自訂的管理器物件
        {
            if (customManager != null)
            {
                return customManager;
            }
            return new FileExtensionManager();
        }
        public void SetManager(IExtensionManager mgr)
        {
            customManager = mgr;
        }
    }

工廠類別千變萬化,這只是其中一個最簡單的呈現。

你唯一要確認的事就是一旦你使用了這些模式,要在工廠類別中加入一個接縫,讓它們可以回傳自訂的虛設常式。


B. 在發布版本中隱藏接縫

使用 [Conditional] 特性:DEBUG 和 RELEASE 是最常見的。
在編譯時,如果這個編譯標記存在,帶標記方法的呼叫端就不會包含在這個編譯的版本中。例如,在編譯 release 版本時,下面這個方法所有的呼叫行為都會被移除,而這個方法內容仍會保留下來。

    [Conditional("DEBUG")]
    public void DoSomething()
    {
        ...
    }

C. 不同的中間層深度等級(淺到深)

這一節中所處理的中間層深度等級與之前幾章節不同,在每一個不同的深度等級,可以選擇產生一個假物件或是虛設常式。

以下列出了三種可在程式中回傳虛設常式的中間層深度等級(淺至深):

  • 層次深度 1:針對類別中的 FileExtensionManager 變數
    • 可進行的操作:新增一個建構函式參數,以便傳入相依物件。此時只有被測試類別中的一個成員是偽造的,其餘的程式碼皆保持不變。
  • 層次深度 2:針對從工廠注入被測試類別的相依物件
    • 可進行的操作:透過工廠類別的賦值方法,設定一個假的相依物件。此時工廠內的成員是偽造的,被測試類別完全不需要調整。
  • 層次深度 3:針對返回相依物件的工廠類別可進行的操作
    • 可進行的操作:將工廠類別直接替換成一個假工廠,假工廠會回傳假的相依物件。此時測試執行過程中,工廠是假的,回傳的物件也是偽造的,被測試類別完全不需要調整。

關於中間層的使用,你需要了解的是,當控制的中間層越深,你對被測試程式的控制能力就越大,但這也同時帶來副作用:中間層越深,測試程式就越難理解,越難找到插入接縫位置。訣竅是要在複雜度與掌控能力之間找到平衡點。程式依然好讀易懂,同時還能完全控制被測試程式的情況。


D. 偽造方法——使用一個區域的工廠方法(擷取與覆寫)

這個跟前面所提的中間層深度相較,並不屬於任何一層,它在接近被測試程式的表層上建立了一個全新的中間層。越接近程式碼的表層,你為了模擬相依物件所需要修改的內容越少。

使用這種方式,你透過被測試類別的一個區域 虛擬方法(virtual method) 作為工廠方法,以獲取檔案類型管理器的物件執行個體。因為這個方法被宣告成虛擬方法,所以它可以在衍生子類別中被覆寫內容,這就產生了所需要的接縫。

你可以在測試中新增一個類別,繼承自被測試類別,並覆寫其虛擬的工廠方法,由此注入假相依物件。

接著,測試程式就可以針對這個新的衍生子類別進行測試。這樣的工廠方法也可以稱為回傳虛設常式方法。

alt text

當繼承被測試類別後,你就可以複寫其虛擬的工廠方法,並自行決定回傳任何一個實作了 IExtensionManager 介面的物件,接著針對剛新增的衍生類別進行測試。

在測試中使用工廠方法步驟:

  • 在被測試類別中:
    • 新增一個新的 虛擬工廠方法,回傳一個實際的物件執行個體
    • 在產品程式碼中正常使用該工廠方法
  • 在測試專案中:
    • 新增一個類別
    • 新的類別 繼承 被測試類別
    • 針對你要取代的介面(IExtensionManager)型別,建立一個公開的爛位
    • 覆寫 虛擬的工廠方法
    • 回傳公開的欄位
  • 在測試程式中:
    • 初始化一個實作 IExtensionManager 的虛設常式物件
    • 初始化在測試專案中所新增的衍生類別物件,而非被測試類別
    • 將虛設常式物件,透過衍生類別物件的公開欄位,注入至衍生類別物件中
β4.D.1: 偽造一個工廠方法
    public class LogAnalyzerUsingFactoryMethod
    {
        public bool IsValidLogFileName(string fileName)
        {
            return GetManager().IsValid(fileName); // 使用虛擬的 GetManager() 方法
        }
        protected virtual IExtensionManager GetManager()
        {
            return new FileExtensionManager(); // 回傳寫死的值
        }
    }

    [TestFixture]
    public class LogAnalyzerTests
    {
        [Test]
        public void overrideTest()
        {
            FakeExtensionmanager stub = new FakeExtensionManager();
            stub.WillBeValid = true;

            TestableLogAnalyzer logan = new TestableLogAnalyzer(stub); // 初始化繼承自被測試類別的衍生類別物件
            bool result = logan.IsValidLogFileName("file.ext");

            Assert.True(result);
        }
    }

    class TestableLogAnalyzer: LogAnalyzerUsingFactoryMethod
    {
        public TestableLogAnalyzer(IExtensionManager mgr)
        {
            manager = mgr;
        }
        public IExtensionManager = Manager;
        protected override IExtensionManager GetManager() // 回傳你指定的值
        {
            return Manager;
        }
    }

    internal class FakeExtensionManager: IExtensionManager // 與前面例子相同不需改變 
    {
        ...
    }

這個技巧稱為「擷取與覆寫(Extract and Override)」,試過幾次你會發現這方法使用上超級簡易。

  • 何時該使用這種擷取與覆寫
    「擷取與覆寫」非常適合用來模擬提供給被測試類別的 輸入(input),但如果要拿來驗證被測試程式對相依物件的呼叫,卻十分不便。
    如果你需要模擬回傳值,或是直接回傳介面,擷取與覆寫都很合適。但如果是要確認被測試類別與相依物件之間的互動,這方式就不適用。

三、克服封裝的問題

  • 使用 internal 和 [InternalsVisibleTo]
  • 使用 [Conditional] 特性
  • 使用 #if 和 #endif 進行條件編譯

四、小節

在開始官方總結之前,我先說一下我自己的心得:「這章節好難!爆幹難!」我也鉅細靡遺看了三遍還似懂非懂,仍在揣摩那傳說中的藝術。不過呢~在心裡埋顆種子,也許哪天突然就發芽了!

在前面兩個章節(Chap 1 & 2),你開始撰寫簡單的測試程式,但有些相依物件要解決、替換。在本章,你已經學會如何使用介面和繼承,透過虛設常式物件來解決直接相依的問題。

在程式中注入虛設常式物件的方式有很多種,關鍵在於找到合適的中間層,或是建立出這個中間層,然後把它拿來當作接縫,在執行過程注入虛設常式內。

因為假物件並不一定為虛設常式(stub)或模擬物件(mock),因此我們命名時習慣用 Fake

擷取與覆寫方式,是為被測試類別模擬輸入的極佳方式。

和經典的物件導向相比,可測試的物件導向設計(TOOD)有些有趣的優點,如讓程式兼具可測試性與可維護性。

下一章節,我們將了解和依賴相關的其他問題,並找到解決這些問題的方式。

#Unit Test #單元測試的藝術







你可能感興趣的文章

ConQuest計分檔的分割函數

ConQuest計分檔的分割函數

The Wonders of Watchessy: Navigating the Maze of Mechanical Mastery

The Wonders of Watchessy: Navigating the Maze of Mechanical Mastery

巨量資料 & 機器學習基礎教學

巨量資料 & 機器學習基礎教學






留言討論