Martin Fowler的一篇文章。
??Key point: two differences; SUT
??'Mock Objects'這個術(shù)語最近經(jīng)常用來描述某些在測試中用來模擬真實Object的特殊對象。很多語言現(xiàn)在都有自己的框架使得mock object更加簡單。然而,很多人經(jīng)常不了解的是,mock object僅僅只是一種形式的特殊測試對象,一個帶來了不同的測試風(fēng)格的Object。 這篇文章中,我將解釋:
mock objects是如何工作的
mock object是如何鼓勵基于行為驗證的測試的
以及如何用它們來進(jìn)行一種不同形式的測試。
我第一次遇到"mock object"這個術(shù)語是在幾年前的一次 Extreme Programming (XP) 社區(qū)中。自那之后,我越來越多地遇到mock objects. 一部分原因是很多主導(dǎo)mock object的開發(fā)人員是我在ThoughtWorks的同事;另一部分原因是 我越來越多地在XP-influenced測試講座中見到它們。
??然而,我很少見到有關(guān)mock object的描述。尤其是,我經(jīng)常看到它們與stubs(一個常見的測試環(huán)境中的helper)的混淆。我理解這種混淆——我也曾經(jīng)覺得它們很相似,但是與mock developers的交流讓我對mock有了更多的一點了解。
??事實上有兩點區(qū)別。一方面,在如何對測試結(jié)果進(jìn)行驗證方面的不同:狀態(tài)驗證(state verification)和行為驗證(behavior verification)的區(qū)別;另一方面,在測試方式和設(shè)計的整體哲學(xué)方面的區(qū)別,我把它們稱為classical style和mockist style(Test Driven Development)。
Regular Tests
我會通過一個簡單的例子來解釋這兩種風(fēng)格。我們想獲取一個order對象,并且從warehouse對象中獲取。這個order很簡單,有一個product以及quantity。warehouse 負(fù)責(zé)各種products的庫存。當(dāng)我們請求order對象從warehouse中獲取product來填充自己時,有兩種可能的回應(yīng)。
- product充足,order得到滿足,warehouse中相應(yīng)product的數(shù)量減少
- 庫存不足,order沒有被滿足,warehouse中不會發(fā)生任何事情。
這兩種行為會有一系列的測試,JUnit測試的代碼可能如下。
public class OrderStateTester extends TestCase {
private static String TALISKER = "Talisker";
private static String HIGHLAND_PARK = "Highland Park";
private Warehouse warehouse = new WarehouseImpl();
protected void setUp() throws Exception {
warehouse.add(TALISKER, 50);
warehouse.add(HIGHLAND_PARK, 25);
}
public void testOrderIsFilledIfEnoughInWarehouse() {
Order order = new Order(TALISKER, 50);
order.fill(warehouse);
assertTrue(order.isFilled());
assertEquals(0, warehouse.getInventory(TALISKER));
}
public void testOrderDoesNotRemoveIfNotEnough() {
Order order = new Order(TALISKER, 51);
order.fill(warehouse);
assertFalse(order.isFilled());
assertEquals(50, warehouse.getInventory(TALISKER));
}
這是一個典型的四階段的測試序列:setup, exercise, verify, teardown。
在這個例子中,setup階段一部分是在setUp方法(set up warehouse)中, 另一部分是在test方法(set up order)中;對order.fill的調(diào)用是在exercise執(zhí)行階段,這是object執(zhí)行我們需要測試的地方;assert語句則是verfify的部分,檢查exercised方法是否被正確執(zhí)行;這個例子中沒有顯式的teardown階段,垃圾收集器隱式地為我們執(zhí)行了。
??在setup的過程中,我們將兩種Object放在了一起。Order是我們的測試對象,但是Order.fill需要一個Warehouse的instance。在這種情況下,Order是我們集中與測試的對象,面向測試(Testing-oriented)的人們喜歡用術(shù)語object-under-test或者system-under-test來描述它。我會使用System Under Test, 或者簡寫為SUT,盡管我個人覺得并不好聽。
??因此對這個test而言,我需要一個SUT(Order)和一個collaborator(warehouse)。我需要warehouse出于兩個原因:一個是為了讓測試行為能夠工作(Order.fill會調(diào)用warehouse的方法),另一個原因是為了verification(因為Order.fill會導(dǎo)致warehouse的某個狀態(tài)的改變)。當(dāng)我們更深入討論這個問題時,你會看到我們會對SUT和collaborator進(jìn)行很多區(qū)分。
??這種風(fēng)格的測試使用的是狀態(tài)驗證:這意味著我們通過檢查SUT和collaborator在方法執(zhí)行之后的狀態(tài)來確認(rèn)執(zhí)行的方法是否正確工作。我們會看到,mock object實現(xiàn)了另一種方式的驗證。
Tests with Mock Objects
現(xiàn)在我會使用mock objects,并作出相同的操作。在這段代碼中,我使用jMock library來定義mocks。jMock是一個Java mock Object的庫。現(xiàn)在又很多mock object的庫,但是jMock是一個由這項技術(shù)的發(fā)起人寫的最新的庫,所以是一個很好的用來作為起點的庫。
public class OrderInteractionTester extends MockObjectTestCase {
private static String TALISKER = "Talisker";
public void testFillingRemovesInventoryIfInStock() {
//setup - data
Order order = new Order(TALISKER, 50);
Mock warehouseMock = new Mock(Warehouse.class);
//setup - expectations
warehouseMock.expects(once()).method("hasInventory")
.with(eq(TALISKER),eq(50))
.will(returnValue(true));
warehouseMock.expects(once()).method("remove")
.with(eq(TALISKER), eq(50))
.after("hasInventory");
//exercise
order.fill((Warehouse) warehouseMock.proxy());
//verify
warehouseMock.verify();
assertTrue(order.isFilled());
}
public void testFillingDoesNotRemoveIfNotEnoughInStock() {
Order order = new Order(TALISKER, 51);
Mock warehouse = mock(Warehouse.class);
warehouse.expects(once()).method("hasInventory")
.withAnyArguments()
.will(returnValue(false));
order.fill((Warehouse) warehouse.proxy());
assertFalse(order.isFilled());
}
首先集中于看testFillingRemovesInventoryIfInStock。setup的階段與之前很不一樣,它分為兩個部分:data(數(shù)據(jù))和expectations(期望)。data的部分set up我們感興趣的用來work的object,和之前的類似;不同之處在于創(chuàng)建的對象。SUT(Order)和之前一樣,但是collaborator不再是warehouse,而是一個mock的warehouse,也就是Mock類的一個instance(Mock warehouseMock = new Mock(Warehouse.class)).
??第二部分是setup會在mock object上建一些expectations。這個expectations表明在SUT執(zhí)行的時候,mock對象的哪些方法需要被調(diào)用。
??當(dāng)所有的expectations都就位之后,我執(zhí)行了SUT。執(zhí)行完成后,我會進(jìn)行verification,這包括兩方面。一方面,我對SUT執(zhí)行了assert——和之前類似。但是,我同樣verify了mock——驗證它們是否根據(jù)expectations被調(diào)用。
關(guān)鍵的不同之處在于我們?nèi)绾未_認(rèn)Order在于warehouse的協(xié)作中做了正確的事情。通過狀態(tài)的驗證,我們利用assert warehouse的狀態(tài)來實現(xiàn)。Mocks使用了behavior verification,其中,我們check Order是否在warehouse上做了正確的調(diào)用——通過在setup過程中告訴mock什么是被期望的,并告訴mock在verification中自行驗證。只有Order是需要通過assert來check的,如果方法沒有改變Order的狀態(tài),則不需要任何assert。.
??在第二個測試中我做了一些別的事情。首先,我創(chuàng)建mock的方式不一樣了,直接使用mock()方法;這個方法的好處是,我不需要顯式地去call verify了, 所有通過mock方法建立的mock對象都會在測試最后自動的verify。(我本可以在第一個測試中也這么做,但是我為了展示顯式地verification)。
??我在第二個測試用例中做的第二個不同的事情是我在expectations中使用了更為寬松的限制——withAnyArguments。這樣就算邏輯修改了,該測試也不需要被修改。
使用EasyMock
如今有很多mock Object的library,有一個我遇到過的是EasyMock,包括Java和.NET版本。EasyMock也實現(xiàn)了行為驗證,但是和jMock在風(fēng)格上有一些值得討論的不同。如下使我們已經(jīng)熟悉的tests:
public class OrderEasyTester extends TestCase {
private static String TALISKER = "Talisker";
private MockControl warehouseControl;
private Warehouse warehouseMock;
public void setUp() {
warehouseControl = MockControl.createControl(Warehouse.class);
warehouseMock = (Warehouse) warehouseControl.getMock();
}
public void testFillingRemovesInventoryIfInStock() {
//setup - data
Order order = new Order(TALISKER, 50);
//setup - expectations
warehouseMock.hasInventory(TALISKER, 50);
warehouseControl.setReturnValue(true);
warehouseMock.remove(TALISKER, 50);
warehouseControl.replay();
//exercise
order.fill(warehouseMock);
//verify
warehouseControl.verify();
assertTrue(order.isFilled());
}
public void testFillingDoesNotRemoveIfNotEnoughInStock() {
Order order = new Order(TALISKER, 51);
warehouseMock.hasInventory(TALISKER, 51);
warehouseControl.setReturnValue(false);
warehouseControl.replay();
order.fill((Warehouse) warehouseMock);
assertFalse(order.isFilled());
warehouseControl.verify();
}
}
EasyMock使用了一個record/replay的比喻來實現(xiàn)expectations。對于每個你希望mock的Object,你需要創(chuàng)建一個control和一個mock object。mock對象滿足collaborator的接口,control則給你提供額外的feature。為了實現(xiàn)一個expectation,你使用你所期望在mock上的參數(shù)調(diào)用方法。如果需要返回值,則需要調(diào)用control對象。
一旦你完成了expectations的設(shè)置,你需要顯示地調(diào)用control.replay()——在該點上,mock的recording結(jié)束,并且可以對SUT作出響應(yīng)。最后,再調(diào)用control.verify()。
??人們第一眼似乎總是對record/replay的比喻充滿畏懼。它與jMock相比有個好處,你是實實在在地在調(diào)用mock的方法,而不是以方法名作為參數(shù)。
?? JMock的developer也正在更新版本以滿足調(diào)用真實方法的需求。
Difference between Mocks and Stubs
當(dāng)?shù)谝淮伪灰霑r,人們總是很容易將mock object和常提及的stubs混淆。然而,為了完全了解如何使用mock,了解mocks和其他類型的test doubles是很重要的。
??當(dāng)你在做這樣的測試:專注于軟件中某個元素的測試——這是常用的術(shù)語unit testing。 問題在于,為了使得某個單獨的unit工作,你經(jīng)常需要其他的units——比如我們例子中的warehouse。
??我上面描述的兩種測試風(fēng)格中,第一個使用了真實的warehouse對象,第二個則mock了warehouse(當(dāng)然這不是一個真正的warehouse對象)。使用mock時不用真實的warehouse的一種方法,但是依然在測試中依然有很多類似的別的形式的非真實的objects。
??我們接下來要討論的詞匯可能會比較混亂——stub, mock, fake, dummy。在這篇文章中,我將會遵循Gerard Meszaros書中的詞匯。它可能不是所用人都使用的,但我覺得還不錯。
Meszaros使用了一個術(shù)語——Test Double作為為了測試目的替代真實Object的所有類型的假的object。他定義了四種特定類型的double:
- Dummy對象是會被傳遞的,但從來不會被真正使用的。它們通常來說僅僅用于填充參數(shù)列表。
- Fake對象事實上會有一些working implementations, 但是通常在實現(xiàn)上走了一些捷徑,從而不適用于production。(an in memory databaseis a good example).
- Stubs在測試中對所有的調(diào)用提供固定的答案,經(jīng)常對于外界的調(diào)用不做任何回應(yīng)。
- Spies是一種stub, 同時還會根據(jù)它是如何被調(diào)用的進(jìn)行一些信息的記錄。比如email service來記錄它發(fā)送了多少消息。
-
Mocks也就是我們這里討論的:被預(yù)先定義好的有expectations的對象,它定義了需要被接收的調(diào)用。
??在以上的這些doubles中,只有mock是堅持在行為驗證上的。其他doubles可以并經(jīng)常用于狀態(tài)驗證。Mocks事實上在執(zhí)行階段和其他doubles的行為是一致的——它們需要讓SUT相信自己適合真正的collaborators工作的,只是mocks在setup和verification階段會有所不同。
Choosing Between the Differences
在這篇文章中,我解釋了很多對的不同:state和behavior verification、classic和mockist TDD。 我們該如何在它們之間做選擇呢? 我從state vs behavior verification開始。
??第一個需要考慮的事情是上下文(context)。我們正在考慮的是一個簡單的collaboration,比如Order與warehouse之間,還是一個棘手的,比如Order和mail service之間。
??如果是一個簡單的collaboration,那么選擇很簡單。如果我是一個classic TDDer,那么我不會用mock,stub或者別的double,我會使用真實的object和state verification。如果我是一個mockist TDDer,我會用Mock和behavior verification。不需要任何抉擇。
??如果是 一個棘手的collaboration,如果我是一個mockist,無需選擇——Mock plus behavior verification。 如果我是一個classicist, 那么我需要作出選擇,但是這個選擇影響并不大。通常來說,classicists會根據(jù)case作出決定,選擇最簡單的方式。
??因此我們可以看出,state vs behavior并不是什么大的決定。關(guān)鍵在于classic和mockist TDD之間的選擇。但是state和behavior verification的特征會影響這個決定,這也是我的關(guān)注點。
??在我對此作出講解之前,首先讓我拋出一個極端例子。有時候,你會發(fā)現(xiàn)很難使用state verification,及時它們不是awkward collaborations。 一個很好的例子是cache。cache本身的一個point就是你無法根據(jù)它的狀態(tài)來判斷它被命中還是miss了——這種情況下使用behavior verification就是一個更明智的選擇。
??在我們深入探討classic/mockist的選擇之前,我們有很多因素要考慮,我把它們分為了幾組。
Driving TDD
?? Mock objects是從XP community中傳出的,并且XP的一個重要原則就是對于Test Driven Development的強(qiáng)調(diào)——一個系統(tǒng)的設(shè)計是在由測試不斷驅(qū)動的迭代中發(fā)展的。因此,對于mockists尤其愛討論在設(shè)計時采用mockist 測試的影響也就不足為奇了。他們尤其提倡使用一種叫做need-driven development的風(fēng)格。
?? 在這種風(fēng)格中,你會從通過撰寫你系統(tǒng)的第一個測試開發(fā)一個 user story開始,實現(xiàn)你SUT的一些接口。通過思考collaborators的一些expectations,你探索SUT和它的協(xié)作者之間的交互——高效地設(shè)計出SUT的外圍接口。
?? 一旦你的第一個test開始run,mocks的expectations提供了下一步驟的規(guī)格(specification),并且是你tests的起點。你將這些expectations變成collaborator的test,并且逐步進(jìn)入系統(tǒng)內(nèi),一次對一個SUT重復(fù)剛剛的步。這個風(fēng)格又被稱為outside-in,一個很好的描述。這種風(fēng)格對分層系統(tǒng)很有用。你從對UI的下層進(jìn)行mock實現(xiàn)編程開始,然后對低一層撰寫test,漸漸地逐步每次實現(xiàn)系統(tǒng)的一層。這是一個很有結(jié)構(gòu)并且很有控制性的方法,一個很多人相信對指導(dǎo)OO和TDD的初學(xué)者很有用的方法。
?? 經(jīng)典的TDD有所不同。你可以做類似的這種逐步方法,用stub代替mock。每當(dāng)你需要collaborator做一些事情使SUT工作,你需要hard-code一些test需要的Response就可以。當(dāng)green之后,你可以使用proper code代替hard code。
但是經(jīng)典的TDD還做了一些別的事情。一種常用的風(fēng)格叫做middle-out。在這種風(fēng)格中,你選擇一些feature,然后決定讓這個feature工作你所需要的domain。你讓這些domain Object做你需要的事情,當(dāng)它們成功工作時,你將UI層置于上方。這樣做的話,你不需要fake任何事。很多人喜歡這么做,因為它將關(guān)注點集中于domain model, 從而使得domain的邏輯與UI分離。
?? 我需要強(qiáng)調(diào)的一點是,mockist和classicsts都是一次集中于一個story。有些學(xué)校認(rèn)為需要一層一層地構(gòu)建應(yīng)用,某一層不完成則另一層也不會開始。Classicists和mockists都是有敏捷開發(fā)的背景,并且更傾向于細(xì)粒度的迭代。因此,它們習(xí)慣于feature by feature的工作而不是layer by layer.
Fixture Setup
?? 當(dāng)你使用經(jīng)典的TDD時,你需要創(chuàng)建的除了SUT之外還包括所有SUT需要的在測試中需要響應(yīng)的collaborators。盡管上述案例中只有一對objects,在真實的test中經(jīng)常包括大量的collaborators。通常每run一次tests,這些對象就會被創(chuàng)建和銷毀。
?? 然而,Mokist test,僅僅需要創(chuàng)建SUT和mock它最直接的neighbor。這樣就避免了很多構(gòu)建復(fù)雜fixtures的工作(至少在理論上。我也遇到過很多很復(fù)雜的mock setup,但那可能是由于沒有很好的使用工具)。
在實際中,classic testers更傾向于盡可能地服用復(fù)雜的fixtures。最簡單的方法就是在xUnit的setup方法中對fixture進(jìn)行setup。更多更復(fù)雜的fixture需要被很多測試類使用,在這種情況下你會創(chuàng)建特別的fixture generation classes。我經(jīng)常基于ThoughtWorks XP項目中的命名習(xí)慣把它們稱為Object Mothers。使用mothers對于更大的classic測試時必要的,但是它們是多余的需要被維護(hù)的code,任何對它們的改變都會對測試有很大的連鎖反應(yīng)。
?? 因此我經(jīng)常聽到這兩種風(fēng)格之間相互指責(zé),Mockists認(rèn)為創(chuàng)建fixture需要很大的effort,然而classicists說這是可以被復(fù)用的,mock卻需要在每個test中被創(chuàng)建。