尽管现在已经有了大量的软件开发方法论及辅助工具帮助开发团队提高软件质量,防止、检测bug,但是一些很简单实用的手段依然是提升软件质量必须的手段,比如单元测试,比如Code review。
单元测试是一种很基本的软件质量保证方法,随着敏捷开发、持续集成的流行,一个高质量软件,如果没有单元测试是无法想象的。比如Java,C#都有非常成熟的单元测试框架,Google也开源了他们的C++单元测试框架以及mock框架。
这篇文章将探讨几个方面的问题,什么是单元测试?单元测试为何如此重要?什么时候进行单元测试?如何进行单元测试?单元测试要注意避免什么问题?
什么是单元测试?其实单元测试的本质就是assert,比如c语言就有内置的assert函数,在不满足某个条件的时候返回错误码,而MFC内置的ASSERT和VERIFY就更强大。如果你已经使用assert函数,其实你已经进入了单元测试的初级阶段,稍微学习一下就可以掌握单元测试方法了。对于C++编程,可以使用google的googletest框架做单元测试,这种unittest框架的好处是集成了单元测试常见的需求,避免了重复开发。
常见的开发任务大致为两种:维护legacy code或者偶尔加入一些小的feature,另外就是搭建一个新软件。这两种任务都不可避免的要反复修改某个feature代码,或者根据测试人员或用户的反馈修改bug。我们如何保证修改以后的代码没有引入新的问题?如果你说你用人品担保,那我服了。对于一个正规流程来说,应该有一种正式的手段来确保修改一个bug没有引入两个三个新的bug,或没有导致以前正确的功能出错,这就是单元测试的重要性,有了足够的单元测试,你就可以理直气壮的说新代码没有问题。
单元测试另一个重要性是帮助你理清设计。对于反对单元测试的一个常见借口是,我们的应用太复杂了,没法写单元测试。不是应用复杂,其实是软件设计有问题,导致没法测试,可测试性也是软件很重要的一个本质特性。如果设计中保证了某个函数某个接口只完成单一责任,没有过多的耦合依赖,那么测试其实是很简单的事情。
单元测试另一个优点是可以集成到持续集成过程中,或者通过脚本简单快速反复运行,不需要手动干预,这对于提高开发效率而言非常重要。
什么时候我们应该写单元测试?是软件代码都写完了,实际运行的时候再写么?单元测试其实应该在设计阶段就开始写,单元测试完成以后再写实际功能部分代码。设计应该是基于接口设计,单元测试也应该基于接口测试,另外针对某些复杂的内部逻辑,也应该有比较多的单元测试保证覆盖率。对于某些核心部分,单元测试的代码甚至应该超过实际代码。而且要注意的是,应该将单元测试部分的代码与工作代码等同看待,一样要做版本管理放入ClearCase或者SVN,而且单元测试部分的代码也要review,保证测试代码也是正确的。个人感觉单元测试(包括部分集成测试代码)在整个代码实现部分要占30%到40%的任务量,这样才比较正常。引入单元测试会在前期导致一些延迟,这是无法避免的,相应的会大大减少后期的维护工作,这是我自己的亲身体会。
那么该如何写单元测试呢?单元测试能不能测GUI点击输入?首先要明确的一点是,单元测试不会替代其它测试手段,单元测试只是白盒测试的一种。单元测试是由开发者编写测试,保证正确完成某个逻辑功能的一种测试方法。而且单元测试不应该涉及到其它外部依赖或者其它的模块,比如GUI点击、网络通讯、数据库通讯或者需要安装某个第三方软件等等,这就需要开发者做好设计,尽量把可能有耦合依赖的部分提取隔离,在集成测试或者其它测试的时候再检查。
我们用一个简单的例子解释一下。某个GUI界面,当按下一个按钮,它要变成另一种颜色,功能完成以后恢复原状,或者是一个控件允许用户输入,输入完成以后校验,根据校验的结果保存或者提醒用户出现问题。这些都是比较常见的流程。这些流程显然不是原子的(atom),涉及到model、View、control各个方面,某些程序员往往在CXXXDialog这样的类里面实现所有这些功能,还感觉封装的非常好,“这不是面向对象封装了么?我把它们都封装到类里面了啊?!”
我们就拿输入校验来说明一下如何分解这个MVC过程。第一步,用户输入,点击OK。这部分显然是View和Control方面的,这部分可由tester方面做检查测试,开发人员需要保证功能实现完整,简单运行正确即可。第二步,检查输入数据,进行逻辑运算。这部分显然是比较复杂的逻辑,涉及到Model和Control,一般不涉及到界面显示,输入部分就是数据,输出部分就是检查的结果。显然,这部分应该做单元测试。第三步,如果出错,反馈给用户出错的结果。这一部分基本上也是以界面显示为主,是需要tester测试的部分。第四步,数据正确,保存用户的输入。这部分涉及到数据运算,也是可以进行单元测试的,涉及到数据库的部分,可能需要做集成测试。
从前面的分析可以看出,涉及到GUI界面的测试一样可以有单元测试,需要开发人员做更多的工作,抽象逻辑计算部分代码。这不容易实现,但是值得去做。
当进入后期开发阶段,当用户或者测试人员发现问题,开发者就应该把这些问题转化为测试用例,这样既保证了修改后的代码没有导致其他bug重新出现,也是对代码逻辑的一种很好的覆盖。针对某些需要依赖其他模块的功能,我们可以mock接口,也可以编写实现模块间的集成测试。另外,开发者还可以进一步定制自己需要的单元测试框架功能,比如我就针对现在工作的项目,设计了一个灵活的添加测试用例的方案,测试用例用类似ini或者xml格式编写,单元测试程序读取测试用例进行测试。这样引入一个新的测试用例就非常容易方便,不需要修改编译代码。当然这种设计也是针对我们项目的输出主要为COM接口而定制的,更像是一种集成测试。
前面就是一些泛泛之谈,没有涉及到实际技术方法,只是鼓吹了单元测试的优点。希望各位程序员或技术领导能更加重视单元测试,在工作中使用单元测试,让它真正成为日常工作的工具来保证软件的高质量开发。