置换测试:Stub,Mock

从测试的角度看,理想情况下,我们的所作的全部测试都是对应了实际的代码,但这并不适用于实际情况,比如每次测试都去访问数据库,或者都去加载许多和待测代码毫无联系的配置文件,这些不但会增加测试的时间开销,同时也会增加测试用例的开发开销。并且这样也难以模拟一些特殊情况下的测试,比如需要特定的网络接入条件,或者当天是某个特殊日期等等。

这时候我们可以用一些模拟的代码来特换实际代码,从而帮助我们进行测试。前两天我司请来的老师来讲的:Stub和Mock,就是两种这样的模拟代码。

但是Stub和Mock的用法由于相似,都是为了替换外部依赖对象,从而经常被理解混淆,或者根本没有分清。但是实际上这是两种完全不同的事物:

  1. 首先它们对于怎么去确定测试结果使用的方式是不同的,其中一个使用行为去确认(behavior verification),一个使用状态去确认(state verification)
  2. 另一方面这是两种将测试和设计结合在一起的方法,一个是自顶向下,一个是自下而上

简言之,Stub更关注交互行为,为了验证待测系统调用目标系统接口的交互行为,而Mock更关注状态,为了验证待测系统调用了目标系统后,目标系统的状态。

public class OrderStateTester {

    private WareHouse warehouse = new WareHouseImpl();
    
    @Before
    protected void setup() throws Exception {
        warehouse.setSize(50);
        warehouse.setLocation("Shang Hai");
    }

    @Test
    public void testOrderIsFilledIfEnoughInWarehouse() {
        Order order = new Order(50);
        order.fill(warehouse);
        assertTrue(order.isFilled());
        assertEquals(0, warehouse.getInventory());
    }

    @Test
    public void testOrderDoseNotRemoveIfNotEnough() {
        Order order = new Order(51);
        order.fill(warehouse);
        assertFalse(order.isFilled());
        assertEquals(50, warehouse.getInventory());
    }
}

类似这样的TestCase是最常见的一种,可以看到我们需要测试的是Order对象。为了这个测试,需要Order跟Warehouse,需要Warehouse的理由有两个:首先需要通过它来配合测试,其次需要它来进行验证(因为order.fill改变了warehouse对象)
如果使用Mock对象会怎么样呢?有很多可用的mock对象库,Mokito,JMock之类的,如果用jMock写一段测试用例则会是:

  public void testFillingRemovesInventoryIfInStock() {
    Order order = new Order(50);
    Mock warehouseMock = new Mock(Warehouse.class);

    warehouseMock.expects(once()).method("hasInventory")
      .with(eq(content),eq(50))
      .will(returnValue(true));
    warehouseMock.expects(once()).method("remove")
      .with(eq(content), eq(50))
      .after("hasInventory");
    order.fill((Warehouse) warehouseMock.proxy());

    warehouseMock.verify();
    assertTrue(order.isFilled());
  }

可以看到在数据准备的阶段,创建了一个Warehouse类的mock对象,接着设置了Mock的期望,这些期望也就是在测试order时会被执行。
在验证阶段,和之前一样可以跑order对象的断言,其次可以调用mock的verify方法,验证它是否像期望的那样去运行。

关键不同点在于怎么样去验证order在于warehouse的交互中做了正确的事。上面的testcase中,我通过warehouse的状态去验证。

如果对于Stub和Mock还是分不清楚的话,或许可以通过老师举得MailSender的case来解释一番:如果我们有一个发送邮件的服务,会和待测对象交互:

public interface MailSender {
  public void send (Message msg);
}  

如果使用Stub去验证:

public class MailSenderStub implements MailSender {
  private List<Message> messages = new ArrayList<Message>();
  public void send (Message msg) {
    messages.add(msg);
  }
  public int numberSent() {
    return messages.size();
  }
}  
@Test
public void testOrder { 
       Order order = new Order(51); 
       MailSenderStub mailer = new MailSenderStub(); 
       order.setMailer(mailer); 
       assertEquals(1 , mailer.numberSent()); 
} 

我们不去关心它是否会发送给正确的人,或者发送的内容是否正确。
如果使用Mock去验证:

@Test
public void testOrderSendsMailIfUnFilled() { 
    Order order = new Order(51); 
    Mock mailer = mock(MailSender.class); 
    Mock warehouse = mock(Warehouse.class); 
    order.setMailer((MailSender)mailer.proxy()); 
    warehouse.expects(once()).method("hasInventory").withAnyArgument() 
    .will(returnValue(false)); 
    order.fill((Warehouse)warehouse.proxy()) 
} 

两种方法都用了别的代码替代真正的MailSender,不同的是Stub采用行为验证,只要发送了邮件即可,而Mock采用了状态验证。


QQ图片20180719212107.jpg

在重新学习了Stub和Mock之后,之前逐渐混淆的概念又有了新的理解,对于目前维护的系统中测试困难的问题,比如测试一段方法需要许多logger对象,或者需要查询DB,发现可以通过完善Stub来解决,而且由于目前系统的开发背景,logger等无关对象(与代码逻辑无关)的实现在多个项目中都几乎相同,所以可以用一套统一的Stub来实现多个系统的测试。而对于那些我们关心它状态的依赖,例如MessageSender,则可以通过Mock的方式实现并验证。

现在的老项目流传下来的祖传test case几乎没有一个能跑通的,下一步的目标就是保证新代码的测试覆盖率,以及在力所能及的范围里面把老代码的测试也搞起来 : )

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,629评论 18 139
  • 单元测试实践背景 测试环境定位bug时,需要测试同学协助手动发起相关业务URL请求,开发进行远程调试问题:1、远程...
    Zeng_小洲阅读 7,653评论 0 4
  • Martin Fowler的一篇文章。  Key point: two differences; SUT  'M...
    Luna_Lu阅读 1,629评论 0 4
  • Mock 方法是单元测试中常见的一种技术,它的主要作用是模拟一些在应用中不容易构造或者比较复杂的对象,从而把测试与...
    熊熊要更努力阅读 28,340评论 2 25
  • 浩瀚的夜太空,阿媛穿着笨重的太空服在太空中飞行,她在着急的躲避着搜捕者的搜捕,她想不明白自己为什么会在这里,而身后...
    木若语阅读 223评论 0 1