单元测试
在以前传统的开发中,很少有人做单元测试,认为开发人员只要完成开发工作就可以了,至于质量方面的问题全部推给测试人员来完成。
这样就完全把开发和测试相分离,任务分割的很明确。看上去责任很明确。但这样会导致一些问题,所有的defect只能在开发完成以后,交给测试人员测试的时候才能发现。这个时候由于已经到了项目后期,fix defect的成本,代价会比较高,有时甚至需要修改架构方面的东西。而且在fix一个defect之后,不能实时的进行验证,需要测试人员的介入,而且有可能会引入新的defect。
而单元测试在项目初期就会极大发现出很多问题,从而避免这些错误推延到项目完成时的功能测试。而且在每次的fix之后,可以很方便的进行regression 测试,从而在一定程度上避免这次改动产生新的defect。
有了单元测试,首先它可以帮我们把开发初期的一些简单错误避免,如使用空指针,内存访问冲突,代码逻辑正确性等一些初级的错误,它还有帮助开发人员对产品架构的反思,从而很早的就发现一些问题。
我在项目中有很深的亲身体验。下面就对比一下没有单元测试和有单元测试对自己工作的一些影响。
在以前的项目中,主要是从事界面方面的开发,没有什么单元测试,只是在自己开发的过程中进行简单的一些测试,在界面上自己做一些验证,然后就提交代码,让QA测试。这样就会有很多问题。自己手动的在界面上测试也许并不全面,没有考虑到所有情况,经常是一些边界情况。最后有一堆的defect,我分析主要有以下几个方面的defect,
第一,初级的错误,如空指针引用,变量初始化,内存访问冲突等,经常碰到的defect就是无法获取参数,导致的null pointer 异常。
第二,一些逻辑错误,很多人都是拷贝别处的代码,没有仔细修改就使用了,这将导致一些逻辑错我,我曾经就fix了这样的一个重大逻辑错误,其隐藏在最底层数据库层面操作的API函数中,原因明显就是当时拷贝来的时候忘记修改导致的,因为这个defect导致上层的几个很严重defect。
第三,一些边界条件,当然其实这个可以归类到二中,但是我这里强调的是自己主观逻辑。
我们一般都能想到大部分的情况,而漏掉一些特殊的情况,特别是一些边界条件。做过ACM题目的人对这个会有更深刻的印象,往往程序就是在边界点崩溃的。
这些是我们最常见的问题了,我认为单元测试能在很大程度上检查出这些问题,及时修正。
后来,我们项目中开始进行单元测试,在我的项目中单元测试这方面包含很多内容,不只是通过一些case来cover所有的path,来检查代码的逻辑。在这些case的基础上,我们还可以进行内存泄漏检测,程序性能分析等操作,挖掘出潜在的问题,还可以通过对关键模块的性能分析来提高程序运行的性能,从而可以极大的提高产品的质量,保证产品运行的正确性,高效性,健壮性。
由于我的项目是c++方面的一个项目,所以采用了CppUnit 单元测试框架。用起来非常方便,其提供MFC对话框,命令行运行两种方式。自己开发时可以使用MFC对话框模式,这样运行起来比较方便。在项目中后期,可以以命令行方式来实现自动化单元测试,以方便regresssion test。
刚开始我觉得写这些case很麻烦,很简单的函数得写一堆case。但是后来我改变这种想法了,它确实可以帮我们发现很多潜在问题,真的是投入一次,收获颇丰啊。
单元测试一般写好之后不需要改动了,除非是需求发生变化。所以当我们完成了相应的函数之后,即可以进行测试。当有了稍微的改动后,随即运行局部的测试用例,或者全部的测试用例,保证你的改动不会影响其他模块。
写单元测试,我们就得覆盖所有的代码,逻辑分支,这样才能最大限度的发现问题。所以我们一般得确保单元测试的代码覆盖率,分支覆盖率更佳。一般认为代码覆盖率达到80%已经是非常不错了。计算代码覆盖率的工具非常之多,但很多优秀的工具都是收费的,这让人很是郁闷,但也有一些免费的,最方便的就是g++附带的gcov工具了,通过一个编译参数,既可以控制来计算代码覆盖率。Windows上我没有使用过太好的工具,主要是在公司不能用盗版。最后用了一下RTRT的试用版,它是可以,但是驾驭它太难了。如果是C#代码,可以使用Sharpdevelop中自带的代码覆盖率工具计算。Java 代码就更不用说了,工具实在太多了,最常用的就是Junit了。
在单元测试代码覆盖率和分支覆盖率的保证下,我们还可以进行内存泄漏检测,程序性能分析等操作。内存泄漏检测有很多工具,我使用的是Rational purify。它是通过对应用程序进行插桩来实现内存泄漏分析的。虽然它运行的不太稳定,但是一般的问题还是能检查出来的。
说起程序性能分析,就得说起RTRT(Rational Test Real Time)了,功能是很强大,他可以同时进行代码覆盖率,内存分析,程序性能分析等操作,但就是太难用了,而且运行十分不稳定。
目前未使用过类似的更好的工具。
一般情况下,我们只要是有比较高的代码覆盖率就可以在一定程度上保证分支覆盖率,由于计算分支覆盖的工具比较少,所以我们最后没有计算分支覆盖率,把重心放在了代码覆盖率
在一次的努力下,以后有任何改动后都运行所有的ut case。
这样才算是一个完整的开发流程。