在刚过去的一个月中,我完成了一个小软件框架的设计与实现。期间由于并行开发的需要,在没有对代码完成单元测试的情形下我将之check in到了SVN的主干上,随后的心情很是忐忑。因为我知道我一定会犯错(事实也证明在单元测试完成之前就发现了两个缺陷),害怕给他人带来麻烦并影响自己的形象。

另外,由于对刚加入项目的单元测试环境完全不了解,所以在该框架的前期开发工作中我并没有运用单元测试渐进地保证软件质量。其结果可想而知,我花了不少时间去修复编码过程中遗留下来的低级错误。需要指出的是,所在项目是一个大型嵌入式系统,项目编译一次就得20分钟左右,调试效率可以想象。与之不同的是,单元测试可以在Linux/Cygwin这样的环境中完成,加上可以使用gdb进行调试,开发效率提高一个数量级应不是问题。

由于我深刻地体会到了单元测试对工作与生活质量的重要性,所以持“真正高质高效的软件开发工程师,一定是那些深刻理解并切实实施单元测试的人”这一观点。然而,就我过去几年的工作见闻来看,发现身边绝大多数的工程师并没有真正用心去拥抱单元测试。出现这样的状况,我认为存在一定的原由,因此想借本文谈谈一些认识。

有相当一部分工程师是因为并不了解什么是单元测试而没能尝到单元测试的好处。一部分人认为,平时开发工作中的调试其实就是单元测试(参见《明析单元测试》),有的则因为没有花时间学习单元测试而对之不了解。对于这些新手,我并不打算在本文对之“扫盲”,请下载ClearRTOS源码点击下载和本文的附件自行学习。 ClearRTOS 是我为《专业嵌入式软件开发》一书所设计的、可在Cygwin和Linux环境中运行的“实时”操作系统,其中涵盖有单元测试方面的内容,更具体的信息见文后。

另外的一部分人尽管实施过单元测试,但却没能从中受益,甚至得出“单元测试无用”的结论。这部分人的困惑是我最想在这里加以指出的。归结起来,我认为实施过程中的“缝太大”是其中一大主因。

不少团队将项目的产品代码与单元测试代码加以区别对待,这是产生“缝”的第一大根源。表现之一是,编译产品代码与编译单元测试代码采用完全不同的编译环境,程序员在日常工作中需要不停地在两个编译环境中进行切换。这种方式很容易让工程师感到麻烦,甚至因此遭到抵制或弃用。好的方式是,将单元测试代码的编译环境与产品代码进行无缝整合。比如,在嵌入式系统项目中做到运行“make release”或“make debug”实现产品代码编译,运行“make unitest”完成单元测试代码编译(ClearRTOS项目就是这么做的)。做到编译环境的无缝整合需要团队中存在精通编译环境构建语言(比如Makefile)的专家。很不幸的是,这方面的专家少得可怜,团队对这方面知识的精进也因为没有意识到其重要性而缺乏动力。

表现之二是,产品代码与单元测试代码区别维护。采用这种方式的团队,很容易在工作中将单元测试摆到更低的位置,开发过程会先以产品代码为主,然后(有时间时)再补上单元测试代码。这种方式很容易降低实施单元测试的效果,且容易因为产品代码与单元测试代码的不同步而带来更大的维护成本。实际上,实施单元测试的一大好处就是在对产品代码进行变更时,通过及时实施单元测试保证软件质量。及时维护单元测试代码从短期和长期都具有很好的经济性,而非象我们想象的那样成本高昂。

产生“缝”的第二大根源,是因为工程师在单元测试过程中不能方便地获得代码覆盖报告。以我的观点,代码覆盖报告应在开发环境中运行象“make creport”这样的命令而轻松获得(ClearRTOS项目同样实现了这一点)。我看到过一些项目,工程师为了获得覆盖报告,需要登录到一个Web服务器上才能查看,而非在工程师的工作机器上随手获取。

产生“缝”的另一大根源与单元测试的“打桩”方法有关。被广为采用的方法是通过使用象Cmockery这样的单元测试框架以打桩的形式,将被测模块独立出来。这种方式尽管被广泛采用,但我觉得所需付出的成本还是很高的。因为“桩的世界”与“产品代码世界”存在很大的“缝隙”,维护期间需要不停地在两个“世界”进行切换,更好的方式是将桩代码融入到产品代码中(细节我想通过另一篇文章给出)。

尽管我认为单元测试是一种有效的质量保障方法,但其有效性在工程界和学术界都存在一定的争议。也正因此,我正以《软件企业成功实施单元测试的关键因素研究》为题,作为我MBA的毕业论文。恳请您花上5分钟左右参与研究问卷调查(点击这里参与),后面我会通过博客公布研究结果。
 
关于ClearRTOS
ClearRTOS现在是一个开源项目(链接),读者可以通过SVN获取将来的新版本。为了能在ClearRTOS项目中获得HTML格式的代码覆盖报告,读者需在Linux/Cygwin中安装LCOV(链接)。
 
在ClearRTOS项目中获取代码覆盖报告的步骤如下:
    1)运行“tar xzvf ClearRTOS.tar.gz”解压。
    2)运行“cd ClearRTOS/build”进入编译目录。
    3)运行“make unitest”编译单元测试程序。
    4)运行“make test”执行单元测试程序。
    5)运行“make creport”获取单元测试报告。报告可用IE、Chrome等浏览器打开ClearRTOS/build/coverage/index.html文件进行浏览。通过点击网页中的相关链接可以查看到每个源文件的代码覆盖情况。

《专业嵌入式软件开发》相关节选
 
26.3.2 构建有效方法论的核心手段
有了工具和流程并不表示质量保证方法论就一定有效,还需要注意它们的可操作性和易用性。有效的质量保证方法论源于构建手段。
 
26.3.2.1 关键要素有形化
在保证软件质量的关键要素中,大体上可以将其分为有形的和无形的两大类。是否有形是指能否通过一定的具体方法施加影响以保证软件质量。比如,设计能力和编程好习惯一开始就不是一种有形的要素,而工具、文档却是有形的。
 
将无形要素有形化是打造质量保证方法论的核心手段之一。对于好的设计思想这一无形的要素,可以通过抽取设计原则,以文档化的方式使其有形;对于编程好习惯也可以通过文档将其有形化;等等。有形化的要素容易在项目团队中被学习和实践,使这些要素在人的行为中体现“形”。
 
一个好的质量保证体系应尽可能地将无形的要素转换成有形的,以便减少其中难以控制的“艺术”成份而获得良好的可操作性。
 
26.3.2.2 流程和工具的无缝整合
对于质量保证方法论中的工具和流程,应尽可能地将它们与项目开发环境进行无缝整合,无缝整合的目的在于保证易用性。工具和流程只有易于使用,才能在项目团队中最大限度地发挥其价值。另外,将一些重复性的工作自动化,也是提高易用性的一种方式。
 
软件开发是一种将无形的需求有形化的过程,有形化后的产物从项目团队的角度来说就是代码。由于代码是无形需求的外在表现,因此代码的质量对于整个软件产品的质量具有决定性的意义。先不说代码在设计上做得如何,但无论怎样的设计所获得的代码都不应当包含编码错误。因此,为了保证代码的质量,需要通过运用工具和方法来找出其中存在的错误。图26.2示例说明了应与开发环境无缝整合的工具和方法。
 
无缝整合的结果是工程师能轻松地使用它们,并体会到其所带来的益处。不少工具和方法之所以不能被持久地运用而发挥作用大多是因为使用起来太麻烦,乃至工程师还没有尝到 “甜头”就放弃了。软件产品的质量源于对每一个软件模块的质量把控,因此开发环境中无缝整合的工具和方法应被应用于每一个软件模块的编码过程,也只有这样质量管理才能落到实处。
 
26.3.2.3 以单元测试为中心
为了获得更好的易用性和效果,在构建质量保证方法论时应做到以单元测试为中心。请不要将“单元测试为中心”理解为“只要做好单元测试就能保证软件质量”。图26.3示例说明了哪些方法和工具应以单元测试为中心。

前面提到,软件产品质量的保证在很大程度上需要通过代码质量保证加以落实,而单元测试能做到最小粒度的代码功能验证。单元测试需要通过设计测试用例保证代码尽可能多地被执行,这也正是代码覆盖、动态分析和性能分析所需要的。将代码覆盖、动态分析和性能分析以单元测试为中心进行整合,能实现通过进行单元测试“顺便”完成以之为中心的其他质量保证工作。
 
以单元测试为中心这一手段有点抽象,在第29、31和32章读者将看到是如何通过以单元测试为中心方便地完成代码覆盖、动态分析和性能分析的。