利用mock發現介面

ghawkgu發表於2011-11-08

原文:モックによるインターフェイスの発見

引言

前幾天,《Mock Roles, not Objects》一文的日語版《ロールをモックせよ(對角色進行模擬)》公開發表了。這是篇發表於2004年的論文,作者陣容相當豪華,他們是:Steve Freeman、Nat Pryce、Tim Mackinnon、Joe Walnes。另外,Steve Freeman和Nat Pryce還是《Growing Object-Oriented Software, Guided by Tests (Addison-Wesley 大師簽名系列)》(即GOOS)的作者,《Mock Roles, not Object》可謂GOOS的思想根基。

在這篇文章中,我想就《Mock Roles, not Object》(以下略稱為MRnO)所提到的使用Mock的基本思想,順著GOOS的思路繼續深入挖掘一下。

把Mock作為一種設計手段

首先,讓我們看一下“MrnO”中介紹的範例的具體程式碼(不過,這篇部落格中的範例程式碼已經將“MRnO”中的範例移植到了jMock2上)。我們要實現的功能如下:

對於一個用於載入物件的框架,利用鍵值進行查詢,要考慮將搜尋結果快取起來的做法。物件載入後,經過一定時間,其例項會無效。因此,需要經常重新載入物件。

Mock Roles, not Objects(p.7)

首先,寫一個簡單的正常處理。

@Test
public void 載入未快取的物件() throws Exception {

    final ObjectLoader mockLoader = context.mock(ObjectLoader.class);

    context.checking(new Expectations() {
        {
            oneOf(mockLoader).load("KEY1");
            will(returnValue("VALUE1"));
        }
    });

    TimedCache cache = new TimedCache(mockLoader);
    assertThat((String) cache.lookup("KEY1"), is("VALUE1"));
}

上述程式碼的行為是這樣的:呼叫cache.lookup()的時候,會呼叫傳入TimedCache構造方法的mockLoader物件的load方法,這時,mockLoader會返回“VALUE1”,所以lookup方法的返回值也是“VALUE1”。要點在於,這個時候,我們已經發現了ObjectLoader這樣一個協作物件(鄰接物件)。就如下圖這樣:

圖1

這裡要補充一點,雖然ObjectLoader在這裡是一個mock,但是並不能認為它是一個通過外部資源訪問的物件,或是由第三方類庫所提供的物件。為了讓測試目標能夠工作,一邊寫測試一邊發現其必要的協作物件,這是以mock為基礎的TDD過程的核心思想。

然後,第二次呼叫cache.loader()方法時,應該直接訪問快取而不是再次呼叫ObjectLoader,這樣的測試也是必須的,因為只是重複了一下斷言語句(asertion),所以就不在此重複其程式碼了。實現了快取後的程式碼如下:

public class TimedCache {

    final private ObjectLoader loader;
    final private Map cachedValues = new HashMap();

    public TimedCache(ObjectLoader loader) {
        this.loader = loader;
    }

    public Object lookup(String key) {
        if (!cachedValues.containsKey(key)) {
            cachedValues.put(key, loader.load(key));
        }
        return cachedValues.get(key);
    }
}

接著,我們再引入時間的概念。需求中提到:“物件載入後,經過一定時間,其例項會無效。因此,需要經常重新載入物件。”與之對應的測試程式碼如下:

@Test
public void 超時後重新載入快取中的物件() throws Exception {

    final Clock mockClock = context.mock(Clock.class);
    final ObjectLoader mockLoader = context.mock(ObjectLoader.class);
    final ReloadPolicy mockPolicy = context.mock(ReloadPolicy.class);

    final Timestamp loadTime = new Timestamp("2011/09/17 00:00:00.000");
    final Timestamp fetchTime = new Timestamp("2011/09/17 00:00:01.000"); // 1秒後
    final Timestamp reloadTime = new Timestamp("2011/09/17 00:00:02.000"); // 2秒後

    context.checking(new Expectations() {
        {
            exactly(3).of(mockClock).getCurrentTime();
            will(onConsecutiveCalls(returnValue(loadTime), returnValue(fetchTime), returnValue(reloadTime)));

            exactly(2).of(mockLoader).load("KEY");
            will(onConsecutiveCalls(returnValue("VALUE"), returnValue("NEW-VALUE")));

            atLeast(1).of(mockPolicy).shouldReload(loadTime, fetchTime);
            will(returnValue(true));
        }
    });

    TimedCache cache = new TimedCache(mockLoader, mockClock, mockPolicy);
    assertThat("被載入的物件", (String) cache.lookup("KEY"), is("VALUE"));
    assertThat("快取中的物件", (String) cache.lookup("KEY"), is("NEW-VALUE"));
}

圖2

這裡就能看出使用mock的TDD過程中的單元測試與普通的TDD過程中最大的不同點。普通的TDD過程中,是通過“紅燈、綠燈、重構”,尤其是“重構”,漸漸提煉出這樣的協作物件的。首先是要讓程式碼能夠執行,其次再考慮合理分配物件的職責。

與此不同的是,使用mock時,在編寫測試的階段,也就是“紅燈”狀態之前就已經出現協作物件了。實際上,按部就班地嘗試一下你就會發現,如果測試目標和協作物件之間的互動行為不考慮清楚的話,是沒有辦法寫測試的。

也就是說,使用mock進行測試驅動開發時,寫測試的過程亦即設計的過程。但是,在這個過程中所做的設計只有物件之間的互動行為,而沒有出現類的概念。“那麼,該如何設計類呢?”出現這樣的疑問是理所應當的,看了GOOS後就會明白,書中採用的方式是先臨時用匿名類實現介面,以後再為這些匿名類附上合理的名稱,提煉出類。通過這種方式所提煉出來的類,如果仍然覺得其內部的職責比較模糊,那麼就要再次寫單元測試,發現其中的協作關係,如此反覆對類進行精煉。

首先寫驗收測試

這也就是為什麼將這種方法稱為“由外及內”的理由。從離外部(即系統的入口)較近的地方開始些單元測試,一邊尋找介面,一邊向系統內部深入。要這樣做,當然會對“首先從哪裡開始入手寫測試”產生疑問,答案就是“首先寫驗收測試”。用圖示來說明的話,是這樣的吧:

圖3

實際上,為了通過驗收測試,有兩點是必須的,一是要邊寫單元測試邊尋找介面,二是要對職責含糊不清的類反覆進行單元測試和提煉,這是一個二重迴圈。乍看之下,驗收測試和單元測試是完全不同的過程。但是,使用mock的單元測試,既需要考慮與物件之間互動有關的內部關注點,同時又需要考慮讓系統整體正常運作的外在的明確意圖,從這兩點必要性去考慮的的話,這兩個關注點能融為一個過程,這樣的想法也就順理成章了吧。如果把“驗收測試+使用mock的單元測試”作為一整套過程去考慮,寫單元測試的行為本來就相當於重構中的一個環節,也許這麼說也不為過吧。

而且,從驗收測試開始入手這樣的想法,與行為驅動開發(BDD:behaviour-driven development)之間有很強的親和力。也就是說,問題變成了“用測試來展現系統該做什麼”,這樣,該從哪裡開始入手、一個特性怎樣才算完成,同時就能對這兩個問題給出答案了。

總結

對於開發人員而言,通過編寫程式碼發現系統架構的過程,確實感覺非常振奮。但是就現實問題而言,對系統一無所知就開始入手,何時怎樣才算終點也一無所知,確實會有這樣的恐懼感。所以,事先進行一定程度的分析及設計,對系統大體上的組成設立目標是有必要的。要避免飽受批判的“Big Design Up Front”,掌握好事先分析及設計的平衡感是很重要的啊。(這些工作能讓你事半功倍)。

對mock有興趣的讀者,一定要看看《Mock Roles, not Objects》。除了本文中提到的思想外,原文中還有許多重要的啟示。另外,點選這裡可以下載本文中的原始碼

譯註

  1. 原文連結
  2. 《Mock Roles, not Objects》原版
  3. 《Growing Object-Oriented Software, Guided by Tests 》(即GOOS)已經引進,中文版名稱是《測試驅動的物件導向軟體開發》
  4. 圖示部分的沒有翻譯,給出圖中用於的對照表:
    • テスト 測試
    • 受け入れテスト 驗收測試
    • ユニットテスト 單元測試

相關文章