2011年11月4日星期五

Dependency Injection 筆記 (5)

前文,說明 DI 的好處:可測試性。

聲明:這是我閱讀 Dependency Injection in .NET 一書的筆記,寫作的內容架構與靈感多來自本書。若覺得這些筆記對您有幫助,主要應歸功於該書的作者 Mark Seemann。筆記內容可能會持續修訂更新,請勿以複製全文的方式轉載。

可測試性

可測試性(testability)對有些人來說無關緊要,或不可承受之重。對另一些人來說,則是不可或缺的實務,苦口可口的良藥。

應用程式可不可測,這裡特別指的是有沒有單元測試(unit test),而不是類似用滑鼠在視窗或網頁上東點西點那種測試方式。撰寫單元測試需要額外的成本,但是有「寫一次,不斷重複使用」的好處,同時還能夠利用工具來自動執行測試,屬於先苦後甘型的測試。不願意先花點時間寫單元測試的人,也許是因為無暇細想後續的修改與人工測試成本;這種工作方式偏向先甘後苦,或者先苦後也苦。
定義:當我們說某個應用程式是「可測試的」,指的是它有單元測試。
欲做好單元測試,一個要點是「單元」的粗細要能切得恰當,使各單元之間的耦合低,以便單獨測試。當然,「單元」的粒度沒有統一標準,只要它不是橫跨太多模組就好。

DI 能夠促使我們思考如何儘量減少類別之間的耦合,並針對介面(而非實作)來寫程式。光是這點,就已經能夠讓我們在撰寫單元測試時更容易切分測試的單元,以及寫出更乾淨的測試程式碼。不過,在寫單元測試的時候,還是會碰到一些麻煩,例如在呼叫測試方法之前,就必須事先建立好一些要傳入的相依物件。有時候,光是建立相依物件或初始設定的準備工作,可能就令人望之卻步,放棄寫單元測試了。

比如說,先前範例中的 HeavyDuty 類別:

    public class HeavyDuty
    {
        private ILogger logger;
 
        public HeavyDuty(ILogger aLogger)
        {
            logger = aLogger;
        }
 
        public void Run()
        {
            logger.WriteEntry("HeavyDuty is running...");
        }
    }

如果我們只先定義了 ILogger 介面,但還沒有實作任何具象類別,此時若要對 HeavyDuty 類別的 Run 方法撰寫單元測試(在 Run 方法上點右鍵,選  Create Unit Tests),Visual Studio 會幫我們產生如下的測試方法:

    [TestMethod()]
    public void RunTest()
    {
        ILogger aLogger = null// TODO: Initialize to an appropriate value
        HeavyDuty target = new HeavyDuty(aLogger); // TODO: Initialize to an appropriate value
        target.Run();
    }

其實此測試方法的最後還有一行呼叫 Assert 類別的程式碼,由於這裡並沒有任何傳回結果需要比對,簡單起見,我就把它刪掉了。

直接跑這個單元測試,當然不會通過,因為 Run 方法會用到 logger 物件來輸出 log 訊息,而我們的測試程式卻沒有建立實作 ILogger 介面的物件實體,只是單純將 null 傳入 HeavyDuty 的建構式。(註:此系列的第三集有提過,這是 DI 的其中一種寫法,叫做 Constructor Injection)

為了讓這個測試能夠通過,我們可以先寫一個空的類別來實作 ILogger 介面。所謂「空的」,就是有提供 ILogger 介面所規定的屬性和方法,但方法本體都是空的,沒有任何程式碼。

如果受測類別(這裡的 HeavyDuty)所相依的介面規格像 ILogger 這麼單純,如此解法也許還算 OK(不用花太大力氣)。可是如果相依的介面超過一個,而且介面的規格比較複雜呢?難道我們非得將所有相依的介面和類別都實作完,才能寫單元測試嗎?有沒有什麼工具可以協助我們產生這些空的類別,或者,直接建立有實作相依介面的「假物件」?

有的,目前已經有許多協助我們在單元測試中直接建立這類「假物件」的類別庫,例如 Rhino MocksNMockMoq(讀作 "mock-you"  或 "mock")等。這裡的「假物件」(fake)還有個比較通用的稱呼:測試替身(test doubles)。由於它只是用來模擬真實物件的暫時替身,所以還有另一個常見的稱呼:模擬物件(mock object)。

接著就用 Moq來做個簡單示範,看看如何在沒有撰寫任何實作 ILogger 介面的具象類別之前,利用 Moq 來建立測試替身。

使用 Moq 來建立測試替身

這裡要直接修改先前建立好的單元測試專案:加入 Moq 組件參考,然後修改測試方法 RunTest:

using Moq;
....
        [TestMethod()]
        public void RunTest()
        {
            var loggerMock = new Mock<ILogger>();
            HeavyDuty target = new HeavyDuty(loggerMock.Object);
            target.Run();
        }

OK! 這樣就能通過單元測試了。其中的 loggerMock.Object 就是 Moq 幫我們建立的測試替身。

如你所見,我們可以只先定義 ILogger 介面,而無須撰寫任何實作 ILogger 的具象類別,同時透過 Moq 的幫忙,替我們直接建立實作該介面的假物件,也就是測試替身啦。

小結

此系列的上一篇文章有提到 DI 可以協助撰寫晚期繫結的程式碼,從而提升應用程式的彈性。不過,程式是否具備單元測試,恐怕要比物件之間的連結是編譯時期決定(早期繫結)還是執行時期決定(晚期繫結)來得重要許多。

當你開始在程式中加入 DI 的寫法,並逐漸習慣使用 mocking 工具來協助建立測試替身(模擬物件)時,你應該會發現自已越來越容易從用戶端的角度、由上而下的方式去思考,也更容易採用以介面為基礎的寫法。甚至於,你可能會覺得「先寫測試」已經不是那麼高不可攀了 。

喔,其實我並不是「先寫測試」的狂熱分子。
Happy Coding!

9 回應:

真的相當期待老師將這整系列的文章,整理成冊。

應該會是初階與中階SD最需要的一本書了。

Wow! 91 的回應真快! 此系列的這幾篇文章,到目前為止都還僅止於入門層次,我想對你來說應該是小菜一碟吧。如有謬誤,請多指正喔。

只能說對這系列文章相見恨晚啊。

雖然懂老師提的一些觀念,但從老師的精準用字遣詞與淺顯易懂的描述,還有太多太多值得我學習的地方 。

希望有朝一日也能跟老師一樣,是除了自己懂以外,還能讓別人懂

91 兄過獎了。一起努力 ^_^

這本剛發行的時候就拿到也讀過了,非常推薦。

在這本書出來之前,因為使用 Spring 作為 IoC 的主要舞台,花了不少時間讀了 Spring 的文件。
裡面的內容跟一些觀念,小提醒也都很棒。

Manning 這幾年的書,質量俱佳。遙想當年技術類書,首選 O'Reilly。

Manning Dependency Injection 這本書的確值得一讀。

的確,我也覺得這本書寫得不錯,也能適時填補目前台灣的程式設計書籍的技術區塊。所以每看一點,就整理一些筆記和自己的想法。

跟您的經驗雷同,我先前也比較常買歐萊禮的書,還有 Addison Wesley。

To IT Player:
我原先以為你講的書就是我目前在看的 DI in .NET (最近剛出版)。但剛剛突然想到,Manning 在 2009 年有出一本書:Dependency Injection。我猜你指的 DI 書籍應該是後者。

Hi Huanlin 您說對了,我看的是先出版的那一本。十月出版的這一本,其實 MEAP 的時候就有先看了。不管如何,這兩本都是質量兼具的書。您是國內有名的譯者(作者),有打算對這一本進行翻譯嗎?亦或是以您的觀點,出版中文書(紙本或是數位)也許都不錯。不過要擔心的是,這種議題的書,不太好賣就是了。雖然有樓上的 91 哥撰寫了大量的文章來推廣,不過要引起廣大共鳴真的不容易。anyway, 謝謝您願意花時間撰寫這類的文,我繼續長期潛水去 :)

多謝誇獎 :)
曾有翻譯本書的念頭,但這本書真的就如你所說,質量兼具,將近 600 頁,對翻譯來說,這"量"還真不少。而且...雖然我知道台灣仍有開發人員對此議題感興趣(例如您和 91 兄以及其他潛水的朋友),但市場還是太小,只能靠興趣支撐。這個系列的筆記,我自己也不知道能寫幾集。就隨緣吧! ^_^