`

意外收获,关于mock和stub。

阅读更多
记录一些自己的想法,边写边想吧。

这是之前写的一篇东西:从AWDWR中的depot思考软件设计
也许表达得不是很清楚……但我自己确确实实能感觉到自顶向下逐步细化需求的开发方式是很有好处的。

后来回头搜索以前JavaEye上TDD相关的讨论,主要是关于mock的,找到这篇文章:讨论《不要把Mock当作你的设计利器》,以及这篇文章的主角:不要把Mock当作你的设计利器,感觉完全颠覆了我之前对mock的认识。好像大家都在说:只有对UI、第3方接口、I/O对象等这些创建的成本很高的对象有进行mock的必要。而我的认识一直是:当你正在测试的对象要调用另一个对象的时候,就要对另一个对象进行mock,从而隔离另一个对象具体的实现对当前对象的影响,这样使开发人员可以一次只关注一件事,完成一件事之后再去做另一件事。

想到这里,问题又来了:为什么不对String进行mock?String也是“其它对象”啊,难道只是因为它是已经实现了的类?这会脑子里有点乱……mock1234说“凡是考虑测试驱动必从突出集成测试这个概念入手”……Sun实现了String就不用mock?……不是说mock用于隔离其它对象的实现对当前对象的影响么……难道这是错的?

停停,以前写过两篇笔记,里面都用到了mock,回去看看也许能有收获,首先是这篇:写了个Servlet的测试用例,初学单元测试,大家帮我看看。

这里面的mock很自然,确实,response、request这样的对象不好创建,只能mock。那么这个servlet的下面呢?它调用的下一层是什么?没有。是的,没有。。。原来当时把下一层的调用直接给忽略掉了。那么,如果它有下一层的调用的话,该怎么办?mock吗?我又想起来以前写的另一篇曾经自以为正确的TDD过程小demo:http://www.iteye.com/topic/257923

在这里,看看这个所谓的“Service”的测试代码(帖子里写的那整个过程就不看了,完全就不是那么回事):
public class BlogServiceTest extends BaseTest {
	
	private BlogService blogService;
	private BlogDAO blogDAO;
	
	public void setBlogService(BlogService blogService){
		this.blogService = blogService;
	}
	
	@Before
	public void setUp(){
		this.blogService = (BlogService)this.applicationContext.getBean("blogService");
		blogDAO = createMock(BlogDAO.class);
		this.blogService.setBlogDAO(blogDAO);
	}
	
	@After
	public void tearDown(){
		verify(blogDAO);
	}

	/**
	 * 测试保存BLOG
	 */
	@Test
	public void testSaveBlog(){
		Blog blog = new Blog();
		blog.setTitle("title");
		blog.setContent("content");
		blog.setCreatedTime(new Date());

		blogDAO.save(blog);
		
		replay(blogDAO);
		blogService.save(blog);
	}
}


这个测试代码里对“DAO”进行了mock,为了让这个mockDAO能够工作,我又必须知道这个DAO应该调用一个save方法……等等!问题在这!这不是正在写Service的测试吗?怎么深入到DAO的接口设计中去了?测试代码本来只需要知道传什么参数给Service,并且预期测试Service返回什么值就够了,管它DAO调用什么方法干嘛?整个Service的实现对测试代码应该是不可见的。啊,原来问题在这里。我想起来TDD一书中的“可以让测试通过的最小改动”,还有那个int amount=10;以及“通过建立存根(stub)来让测试程序通过编译”,我明白stub的作用了。这里需要的是stub,而不是mock。继续看之前写的这个程序,我又想起了dreamhead说的“没有逻辑的东西为什么要测试呢?”似乎都想通了。

本来晚上是通过google搜索到一篇infoQ的文章:“Classic”与“Mockist”TDD,真的对立么?,思考了一下,觉得自己还是没明白,于是我想写篇日志告诉自己,没有实践,看再多也没办法深切理解别人所说的东西,思而不学则贻,先动手做吧,在实践中学习。结果写着想着,意外收获了这些东西……

回头看看,这篇笔记好像被我写成了“散”文……匆忙记录一下,可能思考得还不够深入,也许还存在错误。嗯,再回去翻一下相关的帖子,然后动手试试吧。
分享到:
评论
11 楼 Hooopo 2010-03-30  
机器人 写道
这个我要好好研究一下,rails中的rspec中有这东东,还没有用过

10 楼 yuan 2010-03-30  
frostred,实在不好意思。之前由于缺乏实践,所以对你所说的东西一知半解,不知道该如何回复。
不过这两天我在写测试代码,今天回头看了您在8楼的回复,获益良多,非常感谢!!
9 楼 机器人 2009-11-13  
这个我要好好研究一下,rails中的rspec中有这东东,还没有用过
8 楼 frostred 2009-11-04  
很高兴前面的一点文字能对你有所帮助。事实上,写东西时候,也是对自己的思想整理和精炼的过程,所以可以说是互相帮助吧。你要是有什么疑问或不同意见,可以指出来,我们可以再深入探讨。

好了,下面说说我对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没什么可怕的
• 明白测试的目的是最重要的
7 楼 yuan 2009-11-04  
hi,frostred,谢谢你的回复,你的观点让我对mock/stub的作用有了更深的认识。另外我更想弄明白的是,mock和stub之间有多大区别,分别使用于什么场景,如果你有时间,希望可以谈谈你的看法
6 楼 frostred 2009-11-04  
你的文章写得很好,不过里面有不少对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”

对不起,没时间了,先写到这。。。
5 楼 yuan 2009-10-01  
总结一下,我的理解是,stub只关注输入和输出,而mock比较偏向于关注交互。mock一般用于测试代码中;而stub则是用于产品代码中,往往stub的出现是为了快速让测试通过,stub一般到最后都会被“真家伙”给替代。
4 楼 yuan 2009-09-29  
在自底向上的集成中,首先编写并集成位于hierarchy底部的类。自底向上集成采用一次一个地添加底层类的方式(而不是一次添加全部底层类),因此它是一种增量集成策略。最初你需要编写test driver(驱动测试的类)来演练这些底层类,随着开发的进行,将开发出的类添加到test driver脚手架中。随着高层类的加入,driver类被替换为“真家伙”。//对这句不太理解,driver不是真家伙么?]
自底向上集成只具有增量集成的一部分优点。它能将错误的可能来源限制到“正在被集成的那一个类”上,因此容易定位错误。可以在项目的早期便开始集成。自底向上集成也能及早演练“可能存在问题的系统接口”。既然系统的局限通常能决定你是否能达到系统的设计目标,那么让系统先完成全套热身运动也是值得去做的。
自底向上集成的主要问题在于,它将重要的高层系统接口的集成工作留到最后才进行。如果系统在高层存在概念上的设计问题,那么要把所有的细节工作都做完,构建才能发现这些问题。如果必需对设计做重大修改,那么底层的一些工作多半就得扔掉了。
自底向上集成要求你在开始集成之前,已经完成整个系统的设计工作。//所谓的预先设计吧
如果你不这么做,那么那些不应该支配设计的假设(assumptions)最终会深深地嵌在底层代码中,引起一种很尴尬的局面:在设计高层的类时,需要想办法绕过(work around)底层类中的问题。“让底层细节驱动高层类的设计”违反了信息隐藏原则和面向对象设计的原则。如果你在开始底层编码之时尚未完成高层类的设计,那么一定会出现大量的问题,与之相比,集成高层类时遇到的问题不过是沧海一粟罢了。
与自顶向下集成的情况一样,纯粹自底向上集成也非常罕见,你可以代之以某种混合式的方法。
3 楼 yuan 2009-09-29  
再摘抄一些代码大全(2)里的东西

page694:在自顶向下的集成中,首先编写并集成位于层次体系顶部的类。顶层类可能是主窗口、程序的控制循环、Java中包含mmain()的对象、Microsoft Windows应用程序的WinMain(),或者类似的东西。为了能演练该顶层类,需要编写一些存根(stub)。然后,随着从上而下地集成各个类,这些“存根类”逐渐替换为实际的类。
自顶向下集成的一个重要方面是,类之间的接口必须仔细定义。调试起来最棘手的错误不是那种影响单个类的错误,而是那种由于类之间微妙的交互作用而出现的错误。仔细地进行接口规格说明(specification)能减少这一问题接口规格说明不是一项集成行为,但要确保明确地说明了接口的规格。
除了具有增量集成的一般优点,自顶向下集成的一个额外优点是,能相对较早地测试系统的控制逻辑。继承体系顶部的所有类都进行了大量演练,因此较大的、概念上的设计问题就能及早暴露出来。
自顶向下集成的另一个优点是,如果你认真地进行了计划,你能在项目早期就完成一个能部分工作的系统。如果用户界面位于顶层,那么你能很快获得一个基本的操作界面,然后再填充细节。“让某些看得见的东西早点工作起来”能提高用户和程序员双方的士气
自顶向下的增量集成也能让你在完成底层的设计细节之前就开始编码。一旦各个部分都开始进行相当底层的细节设计,那么就可以开始实现并集成那些位于更高层的类,不必等到万事俱备。
尽管有以上优点,但是纯粹的自顶向下集成通常也具有一些令人难以容忍的缺点。纯粹的自顶向下集成将棘手的系统接口的演练留到最后才进行。如果系统接口有很多bug,或者性能有问题,那么你通常希望很早就能开始处理它们,而不要等到项目结束。“底层的问题冒上来影响顶层系统”的情况并不罕见,这一情况会导致高层的变动,从而减少进行早期集成的收益。为使冒出这种问题的机会减到最少,须对那些“演练系统接口的类”及早(并仔细地)开展开发者测试和性能分析。
纯粹的自顶向下集成的另一个问题是,你需要满满一卡车的“存根/stub”,用于从上而下的集成工作。很多底层的类尚未被集成,这意味着集成的中间步骤需要用很多的stub。stub本身也是问题,因为它是测试代码,比起精心设计的产品代码来,它更有可能包含着错误。为了支持(集成)新的类,就要编写一些新的stub,而这些新的stub又可能包含错误,这就破坏了增量集成“将错误的来源限制在单个新类中”这一目的。
实现纯粹的自顶向下集成也是近乎不可能的。按常规的自顶向下集成方法,你从顶层开始——称为第1层——然后集成位于下一层(第2层)的所有类。当集成完成第2层的全部类之后,才开始集成第3层的类。纯粹的自顶向下集成的这种僵化形式完全没有道理。很难想象有人会不怕麻烦特意使用纯粹的自顶向下集成。大多人使用一种混合的(hybrid)方法,例如自上而下地分部件集成。
最后,如果系统没有顶层类,那么就不能使用自顶向下集成。在许多交互式系统中,“顶层”的位置带有主观性。在许多系统中,用户界面是顶层。在另外的系统中,main()是顶层。
相对于纯粹的自顶向下集成,“竖直分块/vertical-slice”集成是一种很好的替代品。按这种集成步骤,系统是分部件自上而下实现的,多半先充实(完成)一块功能,再转而进行下一块功能。
尽管纯粹的自顶向下集成行不能,但是思考它也有助于你决定使用哪种通用的方式。某些适用于纯粹自顶向下集成的益处和风险同样适用于(虽然不太明显)较为宽松的自顶向下集成方法(例如竖直分块集成),所以请谨记这些益处和风险。
1 楼 yuan 2009-09-17  
由于草稿是在昨天晚上打的,今天修改了半天才发布,所以发布时间变成14:25了……

相关推荐

Global site tag (gtag.js) - Google Analytics