Mocks Aren't Stubs

Martin Fowler的一篇文章。
  Key point: two differences; SUT
  'Mock Objects'这个术语最近经常用来描述某些在测试中用来模拟真实Object的特殊对象。很多语言现在都有自己的框架使得mock object更加简单。然而,很多人经常不了解的是,mock object仅仅只是一种形式的特殊测试对象,一个带来了不同的测试风格的Object。 这篇文章中,我将解释:
mock objects是如何工作的
mock object是如何鼓励基于行为验证的测试的
以及如何用它们来进行一种不同形式的测试。

我第一次遇到"mock object"这个术语是在几年前的一次 Extreme Programming (XP) 社区中。自那之后,我越来越多地遇到mock objects. 一部分原因是很多主导mock object的开发人员是我在ThoughtWorks的同事;另一部分原因是 我越来越多地在XP-influenced测试讲座中见到它们。
  然而,我很少见到有关mock object的描述。尤其是,我经常看到它们与stubs(一个常见的测试环境中的helper)的混淆。我理解这种混淆——我也曾经觉得它们很相似,但是与mock developers的交流让我对mock有了更多的一点了解。
  事实上有两点区别。一方面,在如何对测试结果进行验证方面的不同:状态验证(state verification)和行为验证(behavior verification)的区别;另一方面,在测试方式和设计的整体哲学方面的区别,我把它们称为classical style和mockist style(Test Driven Development)。

Regular Tests

我会通过一个简单的例子来解释这两种风格。我们想获取一个order对象,并且从warehouse对象中获取。这个order很简单,有一个product以及quantity。warehouse 负责各种products的库存。当我们请求order对象从warehouse中获取product来填充自己时,有两种可能的回应。

  1. product充足,order得到满足,warehouse中相应product的数量减少
  2. 库存不足,order没有被满足,warehouse中不会发生任何事情。

这两种行为会有一系列的测试,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的调用是在exercise执行阶段,这是object执行我们需要测试的地方;assert语句则是verfify的部分,检查exercised方法是否被正确执行;这个例子中没有显式的teardown阶段,垃圾收集器隐式地为我们执行了。
  在setup的过程中,我们将两种Object放在了一起。Order是我们的测试对象,但是Order.fill需要一个Warehouse的instance。在这种情况下,Order是我们集中与测试的对象,面向测试(Testing-oriented)的人们喜欢用术语object-under-test或者system-under-test来描述它。我会使用System Under Test, 或者简写为SUT,尽管我个人觉得并不好听。
  因此对这个test而言,我需要一个SUT(Order)和一个collaborator(warehouse)。我需要warehouse出于两个原因:一个是为了让测试行为能够工作(Order.fill会调用warehouse的方法),另一个原因是为了verification(因为Order.fill会导致warehouse的某个状态的改变)。当我们更深入讨论这个问题时,你会看到我们会对SUT和collaborator进行很多区分。
  这种风格的测试使用的是状态验证:这意味着我们通过检查SUT和collaborator在方法执行之后的状态来确认执行的方法是否正确工作。我们会看到,mock object实现了另一种方式的验证。

Tests with Mock Objects

现在我会使用mock objects,并作出相同的操作。在这段代码中,我使用jMock library来定义mocks。jMock是一个Java mock Object的库。现在又很多mock object的库,但是jMock是一个由这项技术的发起人写的最新的库,所以是一个很好的用来作为起点的库。

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(数据)和expectations(期望)。data的部分set up我们感兴趣的用来work的object,和之前的类似;不同之处在于创建的对象。SUT(Order)和之前一样,但是collaborator不再是warehouse,而是一个mock的warehouse,也就是Mock类的一个instance(Mock warehouseMock = new Mock(Warehouse.class)).
  第二部分是setup会在mock object上建一些expectations。这个expectations表明在SUT执行的时候,mock对象的哪些方法需要被调用。
  当所有的expectations都就位之后,我执行了SUT。执行完成后,我会进行verification,这包括两方面。一方面,我对SUT执行了assert——和之前类似。但是,我同样verify了mock——验证它们是否根据expectations被调用。
关键的不同之处在于我们如何确认Order在于warehouse的协作中做了正确的事情。通过状态的验证,我们利用assert warehouse的状态来实现。Mocks使用了behavior verification,其中,我们check Order是否在warehouse上做了正确的调用——通过在setup过程中告诉mock什么是被期望的,并告诉mock在verification中自行验证。只有Order是需要通过assert来check的,如果方法没有改变Order的状态,则不需要任何assert。.
  在第二个测试中我做了一些别的事情。首先,我创建mock的方式不一样了,直接使用mock()方法;这个方法的好处是,我不需要显式地去call verify了, 所有通过mock方法建立的mock对象都会在测试最后自动的verify。(我本可以在第一个测试中也这么做,但是我为了展示显式地verification)。
  我在第二个测试用例中做的第二个不同的事情是我在expectations中使用了更为宽松的限制——withAnyArguments。这样就算逻辑修改了,该测试也不需要被修改。

使用EasyMock

如今有很多mock Object的library,有一个我遇到过的是EasyMock,包括Java和.NET版本。EasyMock也实现了行为验证,但是和jMock在风格上有一些值得讨论的不同。如下使我们已经熟悉的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的比喻来实现expectations。对于每个你希望mock的Object,你需要创建一个control和一个mock object。mock对象满足collaborator的接口,control则给你提供额外的feature。为了实现一个expectation,你使用你所期望在mock上的参数调用方法。如果需要返回值,则需要调用control对象。
一旦你完成了expectations的设置,你需要显示地调用control.replay()——在该点上,mock的recording结束,并且可以对SUT作出响应。最后,再调用control.verify()。
  人们第一眼似乎总是对record/replay的比喻充满畏惧。它与jMock相比有个好处,你是实实在在地在调用mock的方法,而不是以方法名作为参数。
   JMock的developer也正在更新版本以满足调用真实方法的需求。

Difference between Mocks and Stubs

当第一次被引入时,人们总是很容易将mock object和常提及的stubs混淆。然而,为了完全了解如何使用mock,了解mocks和其他类型的test doubles是很重要的。
  当你在做这样的测试:专注于软件中某个元素的测试——这是常用的术语unit testing。 问题在于,为了使得某个单独的unit工作,你经常需要其他的units——比如我们例子中的warehouse。
  我上面描述的两种测试风格中,第一个使用了真实的warehouse对象,第二个则mock了warehouse(当然这不是一个真正的warehouse对象)。使用mock时不用真实的warehouse的一种方法,但是依然在测试中依然有很多类似的别的形式的非真实的objects。
  我们接下来要讨论的词汇可能会比较混乱——stub, mock, fake, dummy。在这篇文章中,我将会遵循Gerard Meszaros书中的词汇。它可能不是所用人都使用的,但我觉得还不错。
Meszaros使用了一个术语——Test Double作为为了测试目的替代真实Object的所有类型的假的object。他定义了四种特定类型的double:

  • Dummy对象是会被传递的,但从来不会被真正使用的。它们通常来说仅仅用于填充参数列表。
  • Fake对象事实上会有一些working implementations, 但是通常在实现上走了一些捷径,从而不适用于production。(an in memory databaseis a good example).
  • Stubs在测试中对所有的调用提供固定的答案,经常对于外界的调用不做任何回应。
  • Spies是一种stub, 同时还会根据它是如何被调用的进行一些信息的记录。比如email service来记录它发送了多少消息。
  • Mocks也就是我们这里讨论的:被预先定义好的有expectations的对象,它定义了需要被接收的调用。
      在以上的这些doubles中,只有mock是坚持在行为验证上的。其他doubles可以并经常用于状态验证。Mocks事实上在执行阶段和其他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会根据case作出决定,选择最简单的方式。
  因此我们可以看出,state vs behavior并不是什么大的决定。关键在于classic和mockist TDD之间的选择。但是state和behavior verification的特征会影响这个决定,这也是我的关注点。
  在我对此作出讲解之前,首先让我抛出一个极端例子。有时候,你会发现很难使用state verification,及时它们不是awkward collaborations。 一个很好的例子是cache。cache本身的一个point就是你无法根据它的状态来判断它被命中还是miss了——这种情况下使用behavior verification就是一个更明智的选择。
  在我们深入探讨classic/mockist的选择之前,我们有很多因素要考虑,我把它们分为了几组。

Driving TDD

   Mock objects是从XP community中传出的,并且XP的一个重要原则就是对于Test Driven Development的强调——一个系统的设计是在由测试不断驱动的迭代中发展的。因此,对于mockists尤其爱讨论在设计时采用mockist 测试的影响也就不足为奇了。他们尤其提倡使用一种叫做need-driven development的风格。
   在这种风格中,你会从通过撰写你系统的第一个测试开发一个 user story开始,实现你SUT的一些接口。通过思考collaborators的一些expectations,你探索SUT和它的协作者之间的交互——高效地设计出SUT的外围接口。
   一旦你的第一个test开始run,mocks的expectations提供了下一步骤的规格(specification),并且是你tests的起点。你将这些expectations变成collaborator的test,并且逐步进入系统内,一次对一个SUT重复刚刚的步。这个风格又被称为outside-in,一个很好的描述。这种风格对分层系统很有用。你从对UI的下层进行mock实现编程开始,然后对低一层撰写test,渐渐地逐步每次实现系统的一层。这是一个很有结构并且很有控制性的方法,一个很多人相信对指导OO和TDD的初学者很有用的方法。
   经典的TDD有所不同。你可以做类似的这种逐步方法,用stub代替mock。每当你需要collaborator做一些事情使SUT工作,你需要hard-code一些test需要的Response就可以。当green之后,你可以使用proper code代替hard code。
但是经典的TDD还做了一些别的事情。一种常用的风格叫做middle-out。在这种风格中,你选择一些feature,然后决定让这个feature工作你所需要的domain。你让这些domain Object做你需要的事情,当它们成功工作时,你将UI层置于上方。这样做的话,你不需要fake任何事。很多人喜欢这么做,因为它将关注点集中于domain model, 从而使得domain的逻辑与UI分离。
   我需要强调的一点是,mockist和classicsts都是一次集中于一个story。有些学校认为需要一层一层地构建应用,某一层不完成则另一层也不会开始。Classicists和mockists都是有敏捷开发的背景,并且更倾向于细粒度的迭代。因此,它们习惯于feature by feature的工作而不是layer by layer.

Fixture Setup

   当你使用经典的TDD时,你需要创建的除了SUT之外还包括所有SUT需要的在测试中需要响应的collaborators。尽管上述案例中只有一对objects,在真实的test中经常包括大量的collaborators。通常每run一次tests,这些对象就会被创建和销毁。
   然而,Mokist test,仅仅需要创建SUT和mock它最直接的neighbor。这样就避免了很多构建复杂fixtures的工作(至少在理论上。我也遇到过很多很复杂的mock setup,但那可能是由于没有很好的使用工具)。
在实际中,classic testers更倾向于尽可能地服用复杂的fixtures。最简单的方法就是在xUnit的setup方法中对fixture进行setup。更多更复杂的fixture需要被很多测试类使用,在这种情况下你会创建特别的fixture generation classes。我经常基于ThoughtWorks XP项目中的命名习惯把它们称为Object Mothers。使用mothers对于更大的classic测试时必要的,但是它们是多余的需要被维护的code,任何对它们的改变都会对测试有很大的连锁反应。
   因此我经常听到这两种风格之间相互指责,Mockists认为创建fixture需要很大的effort,然而classicists说这是可以被复用的,mock却需要在每个test中被创建。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • 1.Creating mock objects 1.1Class mocks idclassMock=OCMCla...
    奔跑的小小鱼阅读 2,529评论 0 0
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • JMockit提供了两套API,一套叫做Expectations,用于基于行为的单元测试;一套叫做Faking,用...
    孙兴斌阅读 1,851评论 0 0
  • 转:http://www.jianshu.com/p/d5fca0185e83 Xcode测试 前言 总算在今天把...
    测试小蚂蚁阅读 2,694评论 0 20
  • Startup 单元测试的核心价值在于两点: 更加精确地定义某段代码的作用,从而使代码的耦合性更低 避免程序员写出...
    wuwenxiang阅读 10,035评论 1 27