令狐写了一篇《单元测试》,源于我们上周的一次关于测试的讨论。TR说到的原子性、独立性、正交性的确也都是值得讨论的问题。不过我比较关注的是粒度和覆盖度。
讨 论是缘起于我们几个最近在合作的一个基于Pylons开发的小项目。Pylons本身是一个基于MVC的WEB框架,我们的应用可以简单地分层为: Controller, Function, Model 这样三层。Model里都是表结构的定义,没有什么好测试的;Function部分是主要的功能实现部分,所以我们在这里使用了Python的 unittest框架进行测试;当然,Pylons为Controller的测试提供了一个基于nose的测试环境,不过我们没用——因为在我们的应用中 Controller只是作为View与Function之间的接口。
但是问题就在这里了,实际开发中发现,某人的Controller代码居然也有错误——具体是谁我就不说了。原因自然是因为我们省略了对Controller的测试,所以这里的错误就不能被及时发现,或者说这部分功能代码没有被测试所覆盖到。
令狐在文中说到的第三点——单元测试应该能保证每一个函数的可靠性——应该说是一个完备的测试所要做到的,但是实际上总会有一些取舍。这其中的原因除了一些特别简单而且基本不会修改的函数可以不必测试以外(这点也要小心,有时越是简单的函数越可能出错),另外一部分原因在于一些函数的依赖性,比如对数据库或网络这样的外部资源的依赖。
回到我们那个具体的项目上。
我们当初之所以只对Function部分进行测试,就是因为这部分被我们设计为最主要的功能实现部分,结果现在出现问题——未被测试所覆盖的部分出错了。
最圆满的解决办法就是再给Controller加一层测试,确保测试的完备。但这样的话就要多一些编写测试的工作了。当然,多写点测试也不是什么坏事,但问题在于我前面已经说了,Controller层很薄,没有多少代码,专门为它写一套测试似乎有点多余。
关于测试的量应该控制在多少,ajoo兄的观点很值得参考。如果测试代码的代码量与被测试代码差不多,那么这样的测试似乎就没有意义了。
好吧,剩下的问题就是:如果减少了测试的代码量,如何保证覆盖范围足够大?答案是把粒度放得粗一些。
以这个案例来说,我 的观点是:实现对Controller层的测试,而放弃对Function层的测试——因为Controller层调用了Function层所有功能,也 就是说,测试可以确保同时覆盖Controller层和Function层。但是代价是失去了测试的准确定位功能,一旦出现测试不通过的情况就无法判断问 题是出在Controller层还是出在Function层。不过幸好Controller层很薄。
当然,令狐是不赞成我这个观点的,所以他写了那么一篇长文章。
其实这样的事情我是干过的。前两年为某商业软件写过一个他们的专用协议所需的反弹端口代理程序,因为如前面所说,这个程序有对网络的外部依赖,所以精确的定点测试比较困难,最后我在开发的时候是用了最偷懒的办法,也就是最粗粒度的测试:
用DUnit写了一个独立的测试程序,在其中实现了两个Mock,分别模拟通讯的两端与代理程序之间用专用协议连接,只要这两个Mock之间通过代理的通讯是正确的,就返回测试通过。
最后我就是用这样的方法把那个程序开发完成的。当然,中间也少不了很多DEBUG LOG的分析处理工作。囧。
总的来说,还是不推荐将测试粒度放得太粗,因为这将失去单元测试的一个很大方面的作用,增加DEBUG的工作量。但是同样不应该将粒度做得太细,这样测试就没有意义了。