Mocks Aren't Stubs

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)。

  1. product充足,order得到滿足,warehouse中相應(yīng)product的數(shù)量減少
  2. 庫存不足,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)建。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內(nèi)容

  • 1.Creating mock objects 1.1Class mocks idclassMock=OCMCla...
    奔跑的小小魚閱讀 2,605評論 0 0
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,781評論 18 139
  • JMockit提供了兩套API,一套叫做Expectations,用于基于行為的單元測試;一套叫做Faking,用...
    孫興斌閱讀 1,925評論 0 0
  • 轉(zhuǎn):http://www.lxweimin.com/p/d5fca0185e83 Xcode測試 前言 總算在今天把...
    測試小螞蟻閱讀 2,959評論 0 20
  • Startup 單元測試的核心價值在于兩點: 更加精確地定義某段代碼的作用,從而使代碼的耦合性更低 避免程序員寫出...
    wuwenxiang閱讀 10,127評論 1 27