作為測試的基本概念,在開發(fā)測試中經(jīng)常遇到mock和stub。之前認為自己對這兩個概念已經(jīng)很明白了,但是當(dāng)決定要寫下來并寫清楚以便能讓不明白的人也能弄明白,似乎就很有困難。
試著寫下此文,以檢驗自己是不是真的明白mock和stub。
1) 相同點
先看看兩者的相同點吧,非常明確的是,mock和stub都可以用來對系統(tǒng)(或者將粒度放小為模塊,單元)進行隔離。
在測試,尤其是單元測試中,我們通常關(guān)注的是主要測試對象的功能和行為,對于主要測試對象涉及到的次要對象尤其是一些依賴,我們僅僅關(guān)注主要測試對象和次要測試對象的交互,比如是否調(diào)用,何時調(diào)用,調(diào)用的參數(shù),調(diào)用的次數(shù)和順序等,以及返回的結(jié)果或發(fā)生的異常。但次要對象是如何執(zhí)行這次調(diào)用的具體細節(jié),我們并不關(guān)注,因此常見的技巧就是用mock對象或者stub對象來替代真實的次要對象,模擬真實場景來進行對主要測試對象的測試工作。
因此從實現(xiàn)上看,mock和stub都是通過創(chuàng)建自己的對象來替代次要測試對象,然后按照測試的需要控制這個對象的行為。
2) 不同點
1. 類實現(xiàn)的方式
從類的實現(xiàn)方式上看,stub有一個顯式的類實現(xiàn),按照stub類的復(fù)用層次可以實現(xiàn)為普通類(被多個測試案例復(fù)用),內(nèi)部類(被同一個測試案例的多個測試方法復(fù)用)乃至內(nèi)部匿名類(只用于當(dāng)前測試方法)。對于stub的方法也會有具體的實現(xiàn),哪怕簡單到只有一個簡單的return語句。
而mock則不同,mock的實現(xiàn)類通常是有mock的工具包如easymock, jmock來隱式實現(xiàn),具體mock的方法的行為則通過record方式來指定。
以mock一個UserService, UserDao為例,最簡單的例子,只有一個查詢方法:
public interface UserService {
User query(String userId);
}
public class UserServiceImpl implements UserService {
private UserDao userDao;
public User query(String userId) {
return userDao.getById(userId);
}
//setter for userDao
}
public interface UserDao {
User getById(String userId);
}
stub的標準實現(xiàn),需要自己實現(xiàn)一個類并實現(xiàn)方法:
public class UserDaoStub implements UserDao {
public User getById(String id) {
User user = new User();
user.set.....
return user;
}
}
@Test
public void testGetById() {
UserServiceImpl service = new UserServiceImpl();
UserDao userDao = new UserDaoStub();
service.setUserDao(userDao);
User user = service.query("1001");
...
}
mock的實現(xiàn),以easymock為例,只要指定mock的類并record期望的行為,并沒有顯式的構(gòu)造新類:
@Test
public void testGetById() {
UserDao dao = Easymock.createMock(UserDao.class);
User user = new User();
user.set.....
Easymock.expect(dao.getById("1001")).andReturn(user);
Easymock.reply(dao);
UserServiceImpl service = new UserServiceImpl();
service.setUserDao(userDao);
User user = service.query("1001");
...
Easymock.verify(dao);
}
對比可以看出,mock編寫相對簡單,只需要關(guān)注被使用的函數(shù),所謂"just enough"。stub要復(fù)雜一些,需要實現(xiàn)邏輯,即使是不需要關(guān)注的方法也至少要給出空實現(xiàn)。
2. 測試邏輯的可讀性
從上面的代碼可以看出,在形式上,mock通常是在測試代碼中直接mock類和定義mock方法的行為,測試代碼和mock的代碼通常是放在一起的,因此測試代碼的邏輯也容易從測試案例的代碼上看出來。Easymock.expect(dao.getById("1001")).andReturn(user); 直截了當(dāng)?shù)闹该髁水?dāng)前測試案例對UserDao這個依賴的預(yù)期: getById需要被調(diào)用,調(diào)用的參數(shù)應(yīng)該是"1001",調(diào)用次數(shù)為1(不明確指定調(diào)用次數(shù)時easymock默認為1)。
而stub的測試案例的代碼中只有簡單的UserDao userDao = new UserDaoStub ();構(gòu)造語句和service.setUserDao(userDao);設(shè)置語句,我們無法直接從測試案例的代碼中看出對依賴的預(yù)期,只能進入具體的UserServiceImpl類的query()方法,看到具體的實現(xiàn)是調(diào)用userDao.getById(userId),這個時候才能明白完整的測試邏輯。因此當(dāng)測試邏輯復(fù)雜,stub數(shù)量多并且某些stub需要傳入一些標記比如true,false之類的來制定不同的行為時,測試邏輯的可讀性就會下降。
3. 可復(fù)用性
Mock通常很少考慮復(fù)用,每個mock對象通過都是遵循"just enough"原則,一般只適用于當(dāng)前測試方法。因此每個測試方法都必須實現(xiàn)自己的mock邏輯,當(dāng)然在同一個測試類中還是可以有一些簡單的初始化邏輯可以復(fù)用。
stub則通常比較方便復(fù)用,尤其是一些通用的stub,比如jdbc連接之類。spring框架就為此提供了大量的stub來方便測試,不過很遺憾的是,它的名字用錯了:spring-mock!
4. 設(shè)計和使用
接著我們從mock和stub的設(shè)計和使用上來比較兩者,這里需要引入兩個概念:interaction-based和state-based。
具體關(guān)于interaction-based和state-based,不再本文闡述,強烈推薦Martin Fowler 的一篇文章,"Mocks Aren't Stubs"。地址為http://martinfowler.com/articles/mocksArentStubs.html(PS:當(dāng)在google中輸入mock stub兩個關(guān)鍵字做搜索時,出來結(jié)果的第一條就是此文,向Martin Fowler致敬,向google致敬),英文不好的同學(xué),可以參考這里的一份中文翻譯:http://www.cnblogs.com/anf/archive/2006/03/27/360248.html。
總結(jié)來說,stub是state-based,關(guān)注的是輸入和輸出。mock是interaction-based,關(guān)注的是交互過程。
5. expectiation/期望
這個才是mock和stub的最重要的區(qū)別:expectiation/期望。
對于mock來說,exception是重中之重:我們期待方法有沒有被調(diào)用,期待適當(dāng)?shù)膮?shù),期待調(diào)用的次數(shù),甚至期待多個mock之間的調(diào)用順序。所有的一切期待都是事先準備好,在測試過程中和測試結(jié)束后驗證是否和預(yù)期的一致。
而對于stub,通常都不會關(guān)注exception,就像上面給出的UserDaoStub的例子,沒有任何代碼來幫助判斷這個stub類是否被調(diào)用。雖然理論上某些stub實現(xiàn)也可以通過自己編碼的方式增加對expectiation的內(nèi)容,比如增加一個計數(shù)器,每次調(diào)用+1之類,但是實際上極少這樣做。
6. 總結(jié)
關(guān)于mock和stub的不同,在Martin Fowler的"Mocks Aren't Stubs"一文中,有以下結(jié)束,我將它列出來作為總結(jié):
(1) Dummy
對象被四處傳遞,但是從不被真正使用。通常他們只是用來填充參數(shù)列表。
(2) Fake
有實際可工作的實現(xiàn),但是通常有一些缺點導(dǎo)致不適合用于產(chǎn)品(基于內(nèi)存的數(shù)據(jù)庫就是一個好例子)。
(3) Stubs
在測試過程中產(chǎn)生的調(diào)用提供預(yù)備好的應(yīng)答,通常不應(yīng)答計劃之外的任何事。stubs可能記錄關(guān)于調(diào)用的信息,比如 郵件網(wǎng)關(guān)的stub 會記錄它發(fā)送的消息,或者可能僅僅是發(fā)送了多少信息。
(4) Mocks
如我們在這里說的那樣:預(yù)先計劃好的對象,帶有各種期待,他們組成了一個關(guān)于他們期待接受的調(diào)用的詳細說明。
3) 退化和轉(zhuǎn)化
在實際的開發(fā)測試過程中,我們會發(fā)現(xiàn)其實mock和stub的界限有時候很模糊,并沒有嚴格的劃分方式,從而造成我們理解上的含糊和困惑。
主要的原因在于現(xiàn)實使用中,我們經(jīng)常將mock做不同程度的退化,從而使得mock對象在某些程度上如stub一樣工作。以easymock為例,我們可以通過anyObject(), isA(Class)等方式放寬對參數(shù)的檢測,以atLeatOnce(),anytimes()來放松對調(diào)用次數(shù)的檢測,我們可以使用Easymock.createControl()而不是Easymock.createStrictControl()來放寬對調(diào)用順序的檢測(或者調(diào)用checkOrder(false)),我們甚至可以通過createNiceControl(), createNiceMock()來創(chuàng)建完全不限制調(diào)用方式而且自動返回簡單值的mock,這和stub就幾乎沒有本質(zhì)區(qū)別了。
目前大多數(shù)的mock工具都提供mock退化為stub的支持,比如easyock中,除了上面列出的any***,NiceMock之外,還提供諸如andStubAnswer(),andStubDelegateTo(),andStubReturn(),andStubThrow()和asStub()。
上面也談到過stub也是可以通過增加代碼來實現(xiàn)一些expectiation的特性,stub理論上也是可以向mock的方向做轉(zhuǎn)化,而從使得兩者的界限更加的模糊。