利用mock發現介面
引言
前幾天,《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這樣一個協作物件(鄰接物件)。就如下圖這樣:
這裡要補充一點,雖然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"));
}
這裡就能看出使用mock的TDD過程中的單元測試與普通的TDD過程中最大的不同點。普通的TDD過程中,是通過“紅燈、綠燈、重構”,尤其是“重構”,漸漸提煉出這樣的協作物件的。首先是要讓程式碼能夠執行,其次再考慮合理分配物件的職責。
與此不同的是,使用mock時,在編寫測試的階段,也就是“紅燈”狀態之前就已經出現協作物件了。實際上,按部就班地嘗試一下你就會發現,如果測試目標和協作物件之間的互動行為不考慮清楚的話,是沒有辦法寫測試的。
也就是說,使用mock進行測試驅動開發時,寫測試的過程亦即設計的過程。但是,在這個過程中所做的設計只有物件之間的互動行為,而沒有出現類的概念。“那麼,該如何設計類呢?”出現這樣的疑問是理所應當的,看了GOOS後就會明白,書中採用的方式是先臨時用匿名類實現介面,以後再為這些匿名類附上合理的名稱,提煉出類。通過這種方式所提煉出來的類,如果仍然覺得其內部的職責比較模糊,那麼就要再次寫單元測試,發現其中的協作關係,如此反覆對類進行精煉。
首先寫驗收測試
這也就是為什麼將這種方法稱為“由外及內”的理由。從離外部(即系統的入口)較近的地方開始些單元測試,一邊尋找介面,一邊向系統內部深入。要這樣做,當然會對“首先從哪裡開始入手寫測試”產生疑問,答案就是“首先寫驗收測試”。用圖示來說明的話,是這樣的吧:
實際上,為了通過驗收測試,有兩點是必須的,一是要邊寫單元測試邊尋找介面,二是要對職責含糊不清的類反覆進行單元測試和提煉,這是一個二重迴圈。乍看之下,驗收測試和單元測試是完全不同的過程。但是,使用mock的單元測試,既需要考慮與物件之間互動有關的內部關注點,同時又需要考慮讓系統整體正常運作的外在的明確意圖,從這兩點必要性去考慮的的話,這兩個關注點能融為一個過程,這樣的想法也就順理成章了吧。如果把“驗收測試+使用mock的單元測試”作為一整套過程去考慮,寫單元測試的行為本來就相當於重構中的一個環節,也許這麼說也不為過吧。
而且,從驗收測試開始入手這樣的想法,與行為驅動開發(BDD:behaviour-driven development)之間有很強的親和力。也就是說,問題變成了“用測試來展現系統該做什麼”,這樣,該從哪裡開始入手、一個特性怎樣才算完成,同時就能對這兩個問題給出答案了。
總結
對於開發人員而言,通過編寫程式碼發現系統架構的過程,確實感覺非常振奮。但是就現實問題而言,對系統一無所知就開始入手,何時怎樣才算終點也一無所知,確實會有這樣的恐懼感。所以,事先進行一定程度的分析及設計,對系統大體上的組成設立目標是有必要的。要避免飽受批判的“Big Design Up Front”,掌握好事先分析及設計的平衡感是很重要的啊。(這些工作能讓你事半功倍)。
對mock有興趣的讀者,一定要看看《Mock Roles, not Objects》。除了本文中提到的思想外,原文中還有許多重要的啟示。另外,點選這裡可以下載本文中的原始碼。
譯註
- 原文連結
- 《Mock Roles, not Objects》原版
- 《Growing Object-Oriented Software, Guided by Tests 》(即GOOS)已經引進,中文版名稱是《測試驅動的物件導向軟體開發》
- 圖示部分的沒有翻譯,給出圖中用於的對照表:
- テスト 測試
- 受け入れテスト 驗收測試
- ユニットテスト 單元測試
相關文章
- 利用fiddler工具,mock介面資料Mock
- 利用Easy Mock簡單模擬開發資料介面Mock
- 利用Swagger UI介面文件同步本地Mock資料SwaggerUIMock
- 簡單介紹python中的mock介面開發PythonMock
- 如何利用fiddler做mock測試Mock
- Android MOCK HTTP 介面新方式AndroidMockHTTP
- APP測試的極簡Mock方法——Mock服務端介面APPMock服務端
- 一條命令解決介面MockMock
- 求助大佬 !!!wireshark 怎麼 mock 介面啊Mock
- Mock測試你的Spring MVC介面MockSpringMVC
- Tool Share (001)---介面 mock 工具推薦Mock
- 使用 mock 模擬登入介面資料Mock
- 開發小技巧-mockMock
- mock 請求分發Mock
- 推薦一個線上介面Mock工具fastmockMockAST
- Go 單元測試之mock介面測試GoMock
- 介面測試如何在 json 中引用 mock 變數JSONMock變數
- 介面測試-使用 mock 生產隨機資料Mock隨機
- 介面測試-使用mock生產隨機資料Mock隨機
- 介面測試如何在json中引用mock變數JSONMock變數
- RESTful 介面設計規範與mock的完美結合RESTMock
- java實現zabbix介面開發Java
- 我的 Mock Server - Meow MockMockServer
- MockMock
- Android開發中利用ObjectAnimator實現ArcMenuAndroidObject
- 前端開發 Mock 利器,效率提升 100%!前端Mock
- 使用ABAP實現Mock測試工具MockitoMockito
- 利用VB 指令碼實現TIA 中介面迴圈計數的功能指令碼
- 利用Redis實現高併發計數器Redis
- mock in iOSMockiOS
- Autocad.net利用Xaml建立Ribbon介面
- php利用pcntl擴充套件實現高併發PHP套件
- mockjs 實現前端非侵入式 mock 解決方案MockJS前端
- 手動編寫mock服務(ma-mock)Mock
- 利用Java處理Jmeter介面常用引數JavaJMeter
- 利用SkyWalking UI的api介面學習GraphQLUIAPI
- 利用python-hashlib 做介面驗證tokenPython
- 淺談mockMock
- 利用“匯出函式和DCOM介面”執行穿透指令、實現橫向滲透函式穿透