浅谈mock和stub

作为测试的基本概念,在开发测试中经常遇到mock和stub。之前认为自己对这两个概念已经很明白了,但是当决定要写下来并写清楚以便能让不明白的人也能弄明白,似乎就很有困难。

一、mock和stub的差异

1、相同点

先看看两者的相同点吧,非常明确的是,mock和stub都可以用来对系统(或者将粒度放小为模块,单元)进行隔离。
在测试,尤其是单元测试中,我们通常关注的是主要测试对象的功能和行为,对于主要测试对象涉及到的次要对象尤其是一些依赖,我们仅仅关注主要测试对象和次要测试对象的交互,比如是否调用,何时调用,调用的参数,调用的次数和顺序等,以及返回的结果或发生的异常。但次要对象是如何执行这次调用的具体细节,我们并不关注,因此常见的技巧就是用mock对象或者stub对象来替代真实的次要对象,模拟真实场景来进行对主要测试对象的测试工作。
因此从实现上看,mock和stub都是通过创建自己的对象来替代次要测试对象,然后按照测试的需要控制这个对象的行为。

2、不同点

1) 类实现的方式
从类的实现方式上看,stub有一个显式的类实现,按照stub类的复用层次可以实现为普通类(被多个测试案例复用),内部类(被同一个测试案例的多个测试方法复用)乃至内部匿名类(只用于当前测试方法)。对于stub的方法也会有具体的实现,哪怕简单到只有一个简单的return语句。
而mock则不同,mock的实现类通常是有mock的工具包如easymock, jmock来隐式实现,具体mock的方法的行为则通过record方式来指定。
以mock一个UserService, UserDao为例,最简单的例子,只有一个查询方法:

        public interface UserService {  
            User query(String userId);  
        }  
  
        public class UserServiceImpl implements UserService {  
            private UserDao userDao;   
            public User query(String userId) {  
                return userDao.getById(userId);  
            }  
            //setter for userDao  
        }  
  
        public interface UserDao {  
            User getById(String userId);  
        }

stub的标准实现,需要自己实现一个类并实现方法:

        public class UserDaoStub implements UserDao {  
            public User getById(String id) {  
                User user = new User();  
                user.set.....  
                return user;  
            }  
        }  
  
      
        @Test  
        public void testGetById() {  
            UserServiceImpl service = new UserServiceImpl();  
            UserDao userDao  = new UserDaoStub();  
            service.setUserDao(userDao);  
  
            User user = service.query("1001");  
            ...  
        }

mock的实现,以easymock为例,只要指定mock的类并record期望的行为,并没有显式的构造新类:

        @Test  
        public void testGetById() {  
            UserDao dao = Easymock.createMock(UserDao.class);  
            User user = new User();  
            user.set.....  
            Easymock.expect(dao.getById("1001")).andReturn(user);  
            Easymock.reply(dao);  
  
            UserServiceImpl service = new UserServiceImpl();  
            service.setUserDao(userDao);  
            User user = service.query("1001");  
            ...  
            Easymock.verify(dao)
        }

对比可以看出,mock编写相对简单,只需要关注被使用的函数,所谓"just enough"。stub要复杂一些,需要实现逻辑,即使是不需要关注的方法也至少要给出空实现。

2)测试逻辑的可读性
从上面的代码可以看出,在形式上,mock通常是在测试代码中直接mock类和定义mock方法的行为,测试代码和mock的代码通常是放在一起的,因此测试代码的逻辑也容易从测试案例的代码上看出来。Easymock.expect(dao.getById("1001")).andReturn(user); 直截了当的指明了当前测试案例对UserDao这个依赖的预期: getById需要被调用,调用的参数应该是"1001",调用次数为1(不明确指定调用次数时easymock默认为1)。
而stub的测试案例的代码中只有简单的UserDao userDao = new UserDaoStub ();构造语句和service.setUserDao(userDao);设置语句,我们无法直接从测试案例的代码中看出对依赖的预期,只能进入具体的UserServiceImpl类的query()方法,看到具体的实现是调用userDao.getById(userId),这个时候才能明白完整的测试逻辑。因此当测试逻辑复杂,stub数量多并且某些stub需要传入一些标记比如true,false之类的来制定不同的行为时,测试逻辑的可读性就会下降。

3)可复用性
Mock通常很少考虑复用,每个mock对象通过都是遵循"just enough"原则,一般只适用于当前测试方法。因此每个测试方法都必须实现自己的mock逻辑,当然在同一个测试类中还是可以有一些简单的初始化逻辑可以复用。
stub则通常比较方便复用,尤其是一些通用的stub,比如jdbc连接之类。spring框架就为此提供了大量的stub来方便测试,不过很遗憾的是,它的名字用错了:spring-mock!

4)设计和使用
接着我们从mock和stub的设计和使用上来比较两者,这里需要引入两个概念:interaction-based和state-based。
具体关于interaction-based和state-based,不再本文阐述,强烈推荐Martin Fowler 的一篇文章,"Mocks Aren't Stubs"。地址为http://martinfowler.com/articles/mocksArentStubs.html(PS:当在google中输入mock stub两个关键字做搜索时,出来结果的第一条就是此文,向Martin Fowler致敬,向google致敬),英文不好的同学,可以参考这里的一份中文翻译:http://www.cnblogs.com/anf/archive/2006/03/27/360248.html。

总结来说,stub是state-based,关注的是输入和输出。mock是interaction-based,关注的是交互过程。

5)expectiation/期望
这个才是mock和stub的最重要的区别:expectiation/期望。
对于mock来说,exception是重中之重:我们期待方法有没有被调用,期待适当的参数,期待调用的次数,甚至期待多个mock之间的调用顺序。所有的一切期待都是事先准备好,在测试过程中和测试结束后验证是否和预期的一致。

而对于stub,通常都不会关注exception,就像上面给出的UserDaoStub的例子,没有任何代码来帮助判断这个stub类是否被调用。虽然理论上某些stub实现也可以通过自己编码的方式增加对expectiation的内容,比如增加一个计数器,每次调用+1之类,但是实际上极少这样做。

6)总结
关于mock和stub的不同,在Martin Fowler的"Mocks Aren't Stubs"一文中,有以下结束,我将它列出来作为总结:
(1) Dummy
对象被四处传递,但是从不被真正使用。通常他们只是用来填充参数列表。
(2) Fake
有实际可工作的实现,但是通常有一些缺点导致不适合用于产品(基于内存的数据库就是一个好例子)。
(3) Stubs
在测试过程中产生的调用提供预备好的应答,通常不应答计划之外的任何事。stubs可能记录关于调用的信息,比如 邮件网关的stub 会记录它发送的消息,或者可能仅仅是发送了多少信息。
(4) Mocks
如我们在这里说的那样:预先计划好的对象,带有各种期待,他们组成了一个关于他们期待接受的调用的详细说明。

3、退化和转化

在实际的开发测试过程中,我们会发现其实mock和stub的界限有时候很模糊,并没有严格的划分方式,从而造成我们理解上的含糊和困惑。
主要的原因在于现实使用中,我们经常将mock做不同程度的退化,从而使得mock对象在某些程度上如stub一样工作。以easymock为例,我们可以通过anyObject(), isA(Class)等方式放宽对参数的检测,以atLeatOnce(),anytimes()来放松对调用次数的检测,我们可以使用Easymock.createControl()而不是Easymock.createStrictControl()来放宽对调用顺序的检测(或者调用checkOrder(false)),我们甚至可以通过createNiceControl(), createNiceMock()来创建完全不限制调用方式而且自动返回简单值的mock,这和stub就几乎没有本质区别了。
目前大多数的mock工具都提供mock退化为stub的支持,比如easyock中,除了上面列出的anytimes,NiceMock之外,还提供诸如:
andStubAnswer(),andStubDelegateTo(),andStubReturn(),andStubThrow()和asStub()。
上面也谈到过stub也是可以通过增加代码来实现一些expectiation的特性,stub理论上也是可以向mock的方向做转化,而从使得两者的界限更加的模糊。

三、其它文章摘录

Stub和Mock都是属于测试替身,对类型细分的话可以分为:

  • Dummy Object
  • Fake Object
  • Test Stub
  • Test Spy
  • Mock Object

前四项属于Stub,最后的Mock Object属于Mock。

  • Dummy Object(哑对象)
    测试代码仅仅是需要使用它来通过编译,实际上用不到它。如测试A类的run方法,需要在创建A类的实例时需要传入B类实例,但run方法并没有用到B类实例。在测试时需要传入B类的哑对象new NullB()(如“new A(new NullB())”),让其通过编译。这里的NullB是一个空类,没有具体实现。
  • Fake Object(假对象)
    假对象相对于哑对象来说,要对耦合的组件有一些简单的实现,实现我们在测试中要用到的方法,指定期望的行为(如返回期望的值)。假对象适用于替换产品代码中使用的全局对象,或者创建的类。这里注意的是要先对被替换的全局对象或类进行备份,然后在测试完成后进行恢复。
  • Test Stub(测试桩)
    测试桩与假对象有点类似,也要实现与产品代码耦合的组件,指定期望的行为。这里最大的不同是测试桩需要注入到产品代码中,从而在测试产品代码时替换组件,执行桩的行为。使用测试桩不需要进行备份和还原。
  • Mock Object(模拟对象)
    设定产品代码中耦合的类的期望的行为,然后验证期望的行为是否发生,从而达到测试产品代码行为的目的。适用于验证一些void的行为。例如:在某个条件发生时,要记录Log。这种情景,用stub就很难验证,因为对目标物件来说,沒有回传值,也沒有状态变化,就只能通过mock object來验证目标物件是否正确的与Log介面进行互动。