Mock 和 Stub 的区别

引用自 iteye 上一篇文章的回复

http://yuan.iteye.com/blog/470418

frostred:

你的文章写得很好,不过里面有不少对Mock/Stub的误解。 首先,我们来看看为什么需要Mock/Stub。 抛开TDD, 单从UnitTest的角度来讲,我们说,一个好的测试必须是一个“值得信赖”的测试。做到值得信赖, 它必须: • 当它通过,我们有信心说被测试代码一定工作。 • 当它失败,它一定证明被测试代码是错误的。

其中第二点与Mock/Stub的关系更紧密些。注意,第二点的重点是“被测试代码”,当测试失败是,一定是什么地方出了问题。我们想要知道的是“被测试代码”出了问题,而不是其它的地方。所以,让我们来看看为什么当测试失败时,它不能证明被测试代码是错误的。最常见的原因是:被测试代码用到了不可信赖东西。例如, 我们常听到: • 虽然测试失败,但是我的代码没问题,可能是配置文件让谁改了吧。 • 虽然测试在这台Vista上失败,但在我的XP上通过,是系统问题, 不是代码问题。 • 测试失败是因为用了不一样的数据库,里面的数据不同导致失败。

所以说,外部资源(External Resource)是常见的不可信赖东西之一。 还有一个常见的不可信赖东西,那就是其他的class,尤其是你自己的,未经任何测试的class。例如,你想要测试ClassA,但在ClassA里,它用到了ClassB,ClassB也是你自己写的,而且它没有被测试过。所以,当ClassA的测试失败是,你怎么可能知道是ClassA,还是ClassB出了问题呢?

怎么解决这一问题呢,这就引出了Mock/Stub。我么需要Mock/Stub去代替那些不可信赖东西。到这,我想我们已经可以回答你的“为什么不对String进行mock” 的疑问。你认为String class是“不可信赖东西”吗?你的class 里用到了String, 当测试失败是,你会怀疑是String class 出了为题吗?如果回答是否定的,那我们说 “没必要对String进行mock”

很高兴前面的一点文字能对你有所帮助。事实上,写东西时候,也是对自己的思想整理和精炼的过程,所以可以说是互相帮助吧。你要是有什么疑问或不同意见,可以指出来,我们可以再深入探讨。

好了,下面说说我对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没什么可怕的 • 明白测试的目的是最重要的