单元测试中Mock与Stub的浅析

秋纹
• 阅读 18652

Github Repo

mocksArentStubs

本部分主要介绍所谓的Test Double的概念,并且对其中容易被混用的Mocks与Stubs的概念进行一个阐述。在初期接触到的时候,很多人会把Mock对象与另一个单元测试中经常用到的Stub对象搞混掉。为了方便更好地理解,这里把所有的所谓的Test Double的概念进行一个说明。我们先来看一个常用的单元测试的用例:


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();
  }
}

当我们进行单元测试的时候,我们会专注于软件中的一个小点,不过问题就是虽然我们只想进行一个单一模块的测试,但是不得不依赖于其他模块,就好像上面例子中的warehouse。而在我提供的两种不同的测试用例的编写方案中,第一个是使用了真实的warehouse对象,而第二个使用了所谓的mock的warehouse对象,也意味着并不是一个真正的warehouse。使用Mock对象也是一种常用的在测试中避免依赖真正的对象的方法,不过像这种在测试中不使用真正对象的方法也有很多。

我们经常看到的类似的关联的名词会有:stub、mock、fake、dummy。本文中我是打算借鉴Gerard Meszaros的论述,可能并不是所有人都怎么描述,不过我觉得Gerard Meszaros说的不错。Gerard Meszaros是用Test Double这个术语来称呼这一类用于替换真实对象的模拟对象。Gerard Meszaros具体定义了以下几类double:

  • Dummy : 用于传递给调用者但是永远不会被真实使用的对象,通常它们只是用来填满参数列表。

  • Fake : Fake对象常常与类的实现一起起作用,但是只是为了让其他程序能够正常运行,譬如内存数据库就是一个很好的例子。

  • Stubs : Stubs通常用于在测试中提供封装好的响应,譬如有时候编程设定的并不会对所有的调用都进行响应。Stubs也会记录下调用的记录,譬如一个email gateway就是一个很好的例子,它可以用来记录所有发送的信息或者它发送的信息的数目。简而言之,Stubs一般是对一个真实对象的封装。

  • Mocks : Mocks也就是Fowler这篇文章讨论的重点,即是针对设定好的调用方法与需要响应的参数封装出合适的对象。

在上述这几种doubles中,只有mocks强调行为验证,其他的一般都是强调状态验证。为了更好地描述这种区别,我们会对上面的例子进行一些扩展。一般在真实对象不太好交互或者代码还没有写好的时候,我们会选择使用一个测试的Double。譬如我们需要测试一个发送邮件的程序是不是能够在发送邮件的时候设定正确的顺序,而我们肯定不希望真的发邮件出去,这样会被打死的。因此我们会为我们的email系统来创建一个test double。这里也是用例子来展示mocks与stubs区别的地方:


public interface MailService {
  public void send (Message msg);
}
public class MailServiceStub implements MailService {
  private List<Message> messages = new ArrayList<Message>();
  public void send (Message msg) {
    messages.add(msg);
  }
  public int numberSent() {
    return messages.size();
  }
}                                 

然后就可以进行状态验证了:


class OrderStateTester...

  public void testOrderSendsMailIfUnfilled() {
    Order order = new Order(TALISKER, 51);
    MailServiceStub mailer = new MailServiceStub();
    order.setMailer(mailer);
    order.fill(warehouse);
    assertEquals(1, mailer.numberSent());
  }

当然这是一个非常简单的测试,我们并没有测试它是否发给了正确的人或者发出了正确的内容。而如果使用Mock的话写法就很不一样了:


class OrderInteractionTester...

  public void testOrderSendsMailIfUnfilled() {
    Order order = new Order(TALISKER, 51);
    Mock warehouse = mock(Warehouse.class);
    Mock mailer = mock(MailService.class);
    order.setMailer((MailService) mailer.proxy());

    mailer.expects(once()).method("send");
    warehouse.expects(once()).method("hasInventory")
      .withAnyArguments()
      .will(returnValue(false));

    order.fill((Warehouse) warehouse.proxy());
  }
}

在两个例子中我们都是用了test double来替代真正的mail服务,不同的在于stub是用的状态验证而mock使用的是行为验证。如果要基于stub编写状态验证的方法,需要写一些额外的代码来进行验证。而Mock对象用的是行为验证,并不需要写太多的额外代码。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
3年前
4. Nginx模块
Nginx官方模块1.ngx\_http\_stub\_status\_modulehttp://nginx.org/en/docs/http/ngx\_http\_stub\_status\_module.html。(https://www.oschina.net/action/GoToLink?urlhttp%3A%2
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Stella981 Stella981
3年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Easter79 Easter79
3年前
SpringBoot整合Redis乱码原因及解决方案
问题描述:springboot使用springdataredis存储数据时乱码rediskey/value出现\\xAC\\xED\\x00\\x05t\\x00\\x05问题分析:查看RedisTemplate类!(https://oscimg.oschina.net/oscnet/0a85565fa
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
秋纹
秋纹
Lv1
雁啼红叶天,人醉黄花地,芭蕉雨声秋梦里。
文章
3
粉丝
0
获赞
0