[转]软件测试- 3 - Mock 和Stub的区别
由于一直没有完全搞明白Mock和Stub的区别,所以查了很多文章,而这一篇是做好的:
http://yuan.iteye.com/blog/470418
尤其是8楼,Frostred的发言,描述地相当清楚具体,转帖到此,以作日后的参考:
很高兴前面的一点文字能对你有所帮助。事实上,写东西时候,也是对自己的思想整理和精炼的过程,所以可以说是互相帮助吧。你要是有什么疑问或不同意见,可以指出来,我们可以再深入探讨。
好了,下面说说我对Mock/Stub区别的看法。
首先,我想再强调一下使用Mock/Stub的目的,那就是,去代替那些被测试代码所依赖的,但不可信赖东西。不管这些东西是什麽,当然最终表现出的还是class。 如class BlogDao, 它不可信赖是因为它访问数据库,class ConfigReader, 它不可信赖是因为它访问配置文件。class MyStringParser, 它不可信赖是因为它有很多逻辑,而且还没有对它进行足够的测试。当然,如果你对它进行足够的测试,你也可以认为它可以信赖。例如,你有一个class MyStringProcessor, 它用到了MyStringParser, 当你测试MyStringProcessor 时,你就有选择是隔离MyStringParser 还是不隔离。注意这一选择是建立在你是否认为MyStringParser 可以信赖的基础上,而不是创建的成本是否很高的基础上。当然,在现实生活中,创建的成本高,往往意味着它用到了外部资源,而用到外部资源也就意味着它不可信赖,也就是它必须被隔离。这也是很多人误以为“创建的成本是否很高”就是判断是否需要隔离的条件。
以上又废了好多话强调使用Mock/Stub的目的,不过我一直认为理解目的是最重要的,目的理解了,其它就容易明白了。
Back to Mock/Stub, 不知你注意了没有,我一直没用Mock这个词做动词,我用的是“代替”或 “隔离”,在这里“代替”和“隔离”是一个意思 。(“隔离”或许更准确些,但“代替”更容易理解,而Mock(动词)是一可非常不准确的词)。那么我们用什么来代替或隔离呢?答案是,Stub / Mock objects。那么,为什么会有Stub / Mock 的区别呢。这是因为Stub / Mock 在测试中扮演的角色有细微的差别,这一差别其实又取决于“被隔离对象”在“被测试对象”里扮演角色的差别(对不起写得有点绕嘴,希望你能看明白)。
其实,分得细点,不只有Stub / Mock,还有其它类型。如,在 xUnit Test Patterns 这本书里,它把这类对象统称为Test Double(因为stunt double 在电影里是替身的意思)。具体的类型有
• Dummy Object
• Test Stub
• Test Spy
• Mock Object
• Fake Object
其中,Fake Object是指一个假的(相对于现实要用到的),但完整的实现。如, InMemoryBlogDao,相对于 SqlBlogDao,它不真的访问数据库,但它是一个对 BlogDao 接口 的完整实现。
其他的类型,我认为,Dummy Object,Test Stub,Test Spy基本可以归为Stub,剩下的Mock Object 当然是Mock。
首先看Dummy Object,测试代码需要Dummy Object是因为有了它才能通过编译,测试才能跑起来,但其实测试中可能根本就用不到它。例如,创建BlogService 需要 BlogDao,但你可能测试BlogService 的一个方法,它根本就没用到BlogDao。此时,你可以用 new BlogService(new NullBlogDao()), NullBlogDao 就是Dummy Object,因为它的存在只是为了通过编译,它根本就不参与测试。
Test Stub 参与测试, 但你不在乎它是何时何地以何种方式参与测试的,它的存在是为了让测试跑起来。非常常见的情况是你需要它提供一些返回值。例如,你可以用HttpContextStub 来代替真正的HttpContext, 用它提供例如SessionId, ResquestParameter 之类的值。你的测试可能会用到这些值,但你不会去验证是不是getSessionId() 被调用了,更不会去验证它是何时何地以何种方式被调用的。
Test Spy 不但参与测试,你还要验证它的参与产生了某种结果。如BlogService 例子。你可以定义一个TestSpy:
public class BlogDaoTestSpy implements BlogDao {
public Blog savedBlog = null;
public void save(Blog blog) {
savedBlog = blog;
}
}
那么你的测试可写成这样:
@Test
public void testSaveBlog(){
Blog blog = new Blog();
blogService.save(blog);
assertEqual(blog, ((BlogDaoTestSpy)blogDao).savedBlog);
}
注意,在测试中我们验证了blogService.save(blog) 会导致blogDao的savedBlog 产生变化,但我们不去验证blogDao是以何种形式参与测试而导致这一变化的。
下面终于该Mock Object 出场了,事实上,你的测试就是很好的Mock例子。在测试中,你验证了如果blogService.save(blog) 被调用,blogDao的save(blog) 一定也会被调用,而且被调用时,参数一定是 blog。也就是说,你验证了blogDao必须以这种特定的形式参与测试。
通过比较,我们可以看到,从Dummy Objec到 Mock Object,测试代码对TestDouble 的要求越来越强,验证的内容也越来越强。这种强制约有好处也有坏处,不过,总的来说,我们希望够用就行。换句话来说,如果能验证代码的正确性,如果Stub够用,就不要用Mock,因为Stub比Mock简单,Stub 对被测代码的制约也小的多,所以被测代码改起来也更容易。
为什么有些时候必须用Mock呢?一个常见的情况是需要参与的方法没有返回值。例如blogDAO.save(blog)。首先,我想说的是,这行代码非常重要,是一定要测试到的。不然的话,你把这行从BlogService中删掉,都没有测试报错,这显然不对。问题是blogDAO.save(blog) 没有返回值,我们怎么才能知道它被正确调用了呢。当然,我们可以用TestSpy,象上面的例子。不过,一般的Dynamic Mock framework 都不支持象上面那类的TestSpy,所以你要手写TestSpy。如果你不愿手写,我认为用Mock是完全可以接受的。这里我还想说的是,它没有“深入到DAO的接口设计中去了”, 因为你的测试只是验证blogDao的save(blog) 会被调用,而没有验证save(blog)的结果是不是正确。如果你验证save(blog)的结果是不是正确,那才是“深入到DAO的接口设计中去了”。所以总的来说,我认为你用Mock 测试是perfectl valid。一点小毛病是,在测试中,你没必要去setTitle("title"), setContent("content"),setCreatedTime(new Date()),这些跟你要测的东西没有任何关系。
一些comments:
“测试代码本来只需要知道传什么参数给Service,并且预期测试Service返回什么值就够了,管它DAO调用什么方法干嘛?”
--理想情况下是,问题是, Service没有返回值,更讨厌的是,连DAO都没有返回值,我们有不能让它沉到太平洋里去,那怎么测试,只好用没办法的办法,验证blogDao的save(blog)将 被调用。
“整个Service的实现对测试代码应该是不可见的“
--理想情况下是,现实中常常不是,尤其是Service class 自己没什么逻辑,but just some interaction with other classes. 例如,Service class 接受一个DTO参数, 然后,用Mapper 把它Map 成Entity object,用Validator 去 validate, 用 logger 写 一个 log, 最后用DAO 存到数据库。这种情况下,你不得不做一些基于Mock 的Interactive 测试。
最后,我的观点,
• 尽量少用 Mock
• 该用Mock的时候就用,Mock没什么可怕的
• 明白测试的目的是最重要的
如果参见这篇博文的人想知道更多,或者英文足够好,这篇老外写的文章,非常地不错:
http://martinfowler.com/articles/mocksArentStubs.html,Mocks Aren't Stubs.
文章来源:http://blog.csdn.net/lilybear101/article/details/6938646