最近团队的交付质量下降,客户有所抱怨,紧急情况出现不少,开发人员应付紧急情况压力和很大,也有抱怨,这样就构成恶性循环。后来开会进行反省总结,发现一些原因:
1. 没时间测试,客户需求变更催的急,交付日期紧迫,导致开发人员减少了单元测试,而我们的测试人员只负责后期的集成测试和用户use case测试,覆盖率是有限的。
2. 开发人员单元测试意识薄弱,小函数认为逻辑很简单,根本没必要测;大函数认为太复杂,根本没时间写那么多测试代码覆盖;依赖数据库、socket和其他服务接口的函数则抱怨没法测,单元测试难度太大。
3. 混淆单元测试和集成测试的概念,习惯做集成测试,从用户use case的角度来测试,而不是针对函数进行测试。
4. 团队没有专门的单元测试人员,没有单元测试自动化测试集成服务器和环境,开发工具和制度建设没有跟上。
5. 公司层面不重视,或者提到了要做单元测试,却没有监督检查机制。
大家都知道,交付质量是公司生存之根本,而怎样的质量管理能把软件的bug消除到最低程度?这是一个很大的问题。涉及到一线开发人员的质量,效率,开发习惯,测试队伍建设,开发流程的建设和改进,工具的合理高效实用等很多方面。其中测试环节是最重要的,是质量保证的关键一环。而单元测试是和开发人员最密切相关的测试类型。通常由开发人员编写和执行。由于单元测试通常发生在错误产生之后不久,因此通过单元测试发现错误然后进行修正的代价通常比较小。我们都知道,bug越早发现越好;发现产品中存在的问题越早,开发费用就越低,产品质量就越高,软件发布后维护费用就越低。一个bug被隐藏的时间的越长,修复这个bug的代价就越大。最后才修改一个bug的代价可能是在bug产生时修改它的代价的10倍!因此,单元测试是如此重要,以至于一些极限编程爱好者主张任何未经测试的代码都应该被自动删除。所以加强开发人员的单元测试意识是首要步骤,除此以外,我们制定如下措施:
1. 在公司层面重视单元测试 - 很多开发人员不做单元测试,是因为公司层面就不重视单元测试,没有监督/review/审查机制,所以我们要在公司层面重视单元测试,有资深程序员对年轻程序员定期进行单元测试Review走查,同时在单元测试技术上进行指导,提交代码到代码库的同时必须同时提交测试代码。
2. 加强开发人员的单元测试意识 - 一个bug被隐藏的时间的越长,修复这个bug的代价就越大。最后才修改一个bug的代价可能是在bug产生时修改它的代价的10倍!而单元测试通常发生在错误产生之后不久,因此通过单元测试发现错误然后进行修正的代价通常比较小。单元测试是如此重要,以至于一些极限编程爱好者主张任何未经测试的代码都应该被自动删除。单元测试还能发现代码结构设计的问题,很难测试的代码,往往也是设计糟糕的代码,例如,一个函数引用大量全局变量,依赖大量外部东西,流程混乱,出口众多,这就是很难测试的代码,往往也意外着设计非常糟糕的代码。因此单元测试还能发现代码结构设计的问题。
3. 增强开发人员通过单元测试对自己代码的信心 - 没写一个函数就对它进行单元测试,不仅在最早就消灭了bug,而且使得开发人员对自己的代码很有信心。与之相反的是,前期没写一个函数都不测试,最后整合起来一跑,问题一大堆,而且到时候也想不起来怎么解决;事实上后期往往发现这种系统测试耗费的时间比预想的多得多,有的甚至超出项目预期耗费的测试时间的一倍以上,究其原因,很多“小零件”前期并没有好好严密的进行测试就来到了总装厂进行总装,这样消耗的时间能不多吗?各个“小零件”中边界条件,异常情况,逻辑分支都存在这样那样的缺陷,导致系统千疮百孔,然后各自开始扯皮推脱责任,这样的系统测试的确难度太大。因此必须从源头上消除这种恶性循环。
4. 加强开发人员单元测试的培训 - 如何在vs2008中进行单元测试(步骤真是简单的不能再简单了,大家可以搜索一下<vs2008 单元测试>),也没有以前的private函数测试麻烦的问题;如何用Mock(具体使用开源的MoQ)来模拟数据库操作、Socket操作、文件操作、其他服务的依赖等等(具体使用大家可以搜索一下<MoQ>,http://code.google.com/p/moq/很简单)。如果程序员A需要调用程序员B开发的一个服务接口,而程序员B还在开发这个接口的实现,程序员A使用Mock就对自己的代码进行测试了。
5. 培养开发人员良好的编程习惯 - 优秀的程序员只是因为习惯。例如没写一个函数前都写测试代码,遵循编程规范,清晰的代码逻辑,边界条件的判断和异常处理,事件日志的清晰记录习惯,析构函数,Dispose方法的实现,函数和类的独立性和单一职责,实现方法的效率等,都和程序员的习惯和专业素质有关。良好的编程习惯能尽量保证在一开始写程序时就避免bug的发生。(Careful programmers test early and test often.)。而对开发人员自身的职业生涯来说,良好的编程习惯也是大有帮助的。编写单元测试是一个专业程序员必须做的。
6. 构建自动化测试环境 - 单元测试用例需要一个自动化执行的环境,即一个能够由外部条件触发而自动运行。单元测试用例本身要能够自动化的进行,不能每次运行之前需要先配置好环境之类,也即能满足回归测试的要求。我们使用CruiseControl.NET这个持续集成工具来进行自动化的配置,具体可参见http://ccnetlive.thoughtworks.com/。采用一个定时的方式来发动集成请求,我们把这个时间设置在半夜,具体做的事情如下:先去 SVN上取出最新的源代码和测试代码,然后编译,最后使用NUnit运行生成的测试dll。
良好的单元测试策略给我们增强了对程序的信心,减少了bug的产生及bug的潜伏期。降低修改bug的代价。单元测试不会是项目开发周期的某一个生命周期,它贯穿于项目的整个生命周期,是一个非常重要的日常开发活动。
当然,公司的budget是有限的,时间资源是有限的,开发人员的时间是有限的,不能做到每个函数每条分支路径都覆盖测试到,所以当我们说单元测试是“必须”的时候,最好还是先看看表,数数人头,再摸摸自己的钱包,确定一下哪些是“必须的”,哪些不是“必须的”,掌握好测试的切入点和目标的粒度,在列表里划出优先级,同时标出不可获缺的测试,平衡的艺术难就难在把握分寸。我认为单元测试的必要性,很多时候来自于开发者脑中对该单元的不确定性,增强自己对这个函数的信心,保证自己代码的质量,减少后期出错的概率,把bug消灭在摇篮中,同时也减少自己后期代码交付以后的担忧和恐惧。
有些人会把测试和TDD联系起来,其实TDD是测试驱动开发,重点是“驱动”,不要搞混淆了。你们用不用TDD来驱动那是仁者见仁智者见智的事情,本文所说的单元测试,和TDD没有直接联系。不管你是否TDD,都可以做单元测试。
最后要说的是,流程配合工具,如果用的好,组织内部的软件构件水平可以上升一个台阶,反之,为了UT而UT,把它变成哗众取宠、华而不实的绣花枕头,走形式的流程比没有这个流程还要糟糕。适合的才是最好的。工具好还要用的好,流程好还要执行的好!