【單元測試的藝術】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 #單元測試的藝術







你可能感興趣的文章

Spot the Difference - Difference in Difference

Spot the Difference - Difference in Difference

pure function

pure function

The introduction and difference between class component and function component in React

The introduction and difference between class component and function component in React






留言討論