单元测试是软件开发过程中的一种质量保证手段。最初的来源是想模仿对硬件芯片做单元测试那样,在软件中也能对小的软件单元进行测试,从而保证软件中某个局部设计的正确性。
传统软件单元测试将被测单元的粒度规定为软件中最小的功能模块。对于C语言通常指一个函数,对于Java或者C++语言通常指一个类。
传统做法是针对被测单元的实现细节进行各种白盒测试,即针对被测代码的实现逻辑进行各种分支测试和覆盖测试。
传统的单元测试由于缺乏自动化工具的支持,往往在测试中通过打印输出测试结果,由人工比对每次测试是否成功。
随着技术的进步和人们对软件单元测试方法的发展,现代单元测试的定义已经发生了很大的变化。
单元测试的粒度以软件设计的松耦合边界为粒度,不一定非要局限于函数和类这么小的粒度。例如对C++的类只用对public的接口进行测试,private接口不测试。对于C语言可以只测试每个文件对外提供服务的接口,文件内的私有辅助函数可以不测试。关于测哪些不测试哪些,最终遵循的原则是在降低单元测试成本的情况下让收益最大化。
单元测试最好是针对被测单元的黑盒测试,这样降低由于被测代码实现细节的改动,导致单元测试也联动修改的频率。
借助现代化单元测试框架的帮助,单元测试可做到一键式的可反复的自动化运行。用例执行结果的成功或失败完全由计算机来进行判断,无需人工参与。由于借助现代化单元测试框架,因此用例的编写需要遵循测试框架的要求。
总结一下:我们认为现代化的单元测试的定义应该是:一种满足一键式全自动化运行的软件单元级别的黑盒测试。
我们认为遵循现代单元测试最佳实践的单元测试过程,可以为软件团队带来如下价值:
单元测试可以让软件故障尽早地被发现。按照统计,软件故障发现越晚,成本呈指数趋势上升。良好的单元测试让故障第一时间被发现,避免故障遗留到后期由于定位修复难度带来的更大损失。
单元测试的可回归性,为软件提供了一层安全防护网。这层安全防护网为软件后续的重构和修改提供了安全保障。
单元测试为软件单元如何被使用,天然提供了一份代码样例式的使用手册文档。
如果能以测试驱动开发(Test Driven Development,简称TDD)的方式进行单元测试,那么可以把单元测试变成一种设计行为,可以驱动出更松耦合的代码设计和实现。
我们认为合格的单元测试应该满足以下要求:
随着技术的成熟,单元测试工具现在已经变得很容易获得和使用了。自从Kent Beck(敏捷软件开发方法泰斗,极限编程和测试驱动开发的提出者)为Java语言开发并开源了JUnit框架后,一下子将单元测试带到了一个新的境地。随后其它语言纷纷效仿JUnit推出了自己的开源单元测试框架。人们后来对所有编程语言的这一系列框架起了个统一的名称,叫做xUnit测试框架
。
目前对于任一编程语言,都能找到好几款开源的的xUnit测试框架,那么如何对比并选择合适好用的xUnit框架呢?一般从如下几个维度去评估。
根据上面提到的判断维度,我们分析对比一下当前主流的C/C++ xUnit测试框架。
测试框架特性 | Boost Test | CppUnit | Gtest | TestNgpp |
---|---|---|---|---|
是否开源 | 是 | 是 | 是 | 是 |
自动检测注册 | 良 | 差 | 优 | 优 |
断言能力 | 良 | 较弱 | 优 | 优 |
支持Fixture | 支持 | 支持 | 支持 | 支持 |
支持Suite分组 | 支持 | 支持 | 支持 | 支持 |
支持用例过滤 | 支持 | 支持 | 支持 | 支持 |
测试报表 | 不支持 | 支持 | 支持 | 支持 |
测试能力 | 良 | 良 | 优 | 优 |
用例依赖管理 | 不支持 | 不支持 | 不支持 | 支持 |
沙盒模式 | 不支持 | 不支持 | 不支持 | 支持 |
社区使用程度 | 低 | 一般 | 使用程度很高 | 一般 |
通过上面的分析可以看到,主流的C++ xUnit测试框架都是开源的。其中TestNgpp功能虽然最强大,但是用户较少。Google推出的Gtest框架使用范围最广,社区支持程度也最好,从功能上来说简单易用,作为上手框架最为合适。其它框架由于各种缺陷不建议再选用了。
对于C ++,有很多已建立的框架,包括(但不限于),Google Test,Boost.Test,CppUnit,Cute,很多甚至更多。catch.hpp主要特点是只有一个头文件,加入工程非常简单实现单元测试。所以在小的工程当中我们可以简单的使用这个头文件完成基本的单元测试功能。
在做单元测试的时候避免不了要为被测代码打桩,而mock框架主要是为了简化打桩过程。使用mock框架可以让打桩代码非常容易撰写,而且不会侵入实现代码。比如两个测试用例需要同一个桩函数:函数声明相同但是返回值不同。在没有mock框架的情况下解决这类问题非常麻烦,而mock框架则可以轻而易举的应对此类问题。
Mock框架除了提供打桩的功能外,还提供其它更加强大的功能。例如何以监听用户对打桩代码的调用行为,并监控这些行为是否符合预期。
对于Java语言来说,可用的mock框架五花八门,选择范围非常广。但是对于C++语言来说,只有两款易用的mock框架:gmock和mockcpp。这两款都是开源软件,经过使用对比,mockcpp功能强大且用户体验胜过gmock,所以基本没有什么好对比和推荐的,如果需要直接上mockcpp就好了。
基于前面介绍的xUnit测试框架,为代码做单元测试的过程一般分为如下主要步骤:
这一步是在每个开发人员的机器上搭建单元测试环境。需要做的步骤如下:
当开发人员的机器上已经搭建好单元测试工具后。接下来就可以对代码进行单元测试了。
一般使用xUnit框架进行单元测试主要有以下几个过程:
建立一个单元测试的代码文件,如果是C++的话,那就是一个普通的cpp源码文件;
选择需要测试的对象代码,例如某个接口函数或者某个类。在测试文件中包含待测代码的头文件。
在测试文件里编写测试用例,测试用例一般包含以下几个主要部分:
编写好用例后,调用测试用例的构建脚本,编译及执行用例,看用例是否通过。
如果用例失败看是用例的问题还是被测代码的问题,修复直到用例通过。
将编写好的用例以及修改的代码提交到代码管理仓库。
一般一个大型的软件团队都是多人合作开发的模式,这时会通过公共的代码管理仓库进行协调。项了保证代码每次修改的安全性,需要搭建持续集成服务器。持续集成服务器就是安装了持续集成软件(例如开源的Jenkins软件)的机器。该机器会实时监控代码管理仓库,一旦发现有新的代码提交,就会触发一系列用户定义的持续集成任务(参加下面的示意图)。
以Jenkins举例来说,常见的可配置的持续集成任务包括:
由于持续集成服务器时刻监控代码管理仓库,一旦有新的代码合入就立即执行对应的任务:例如编译、构建、执行所有单元测试用例等等。持续集成工具都支持结果通知的配置,当存在某项任务失败则通过看板或者邮件的方式通知指定负责人,这样一旦有人提交的代码造成编译构建失败或者单元测试失败,就会立即被发现。这样就避免了低质量的软件合入到代码仓库后,到很晚才能知道的问题。
和单元测试相关性较大的一个是测试覆盖率报表的生成。对于C/C++,可选的测试覆盖率工具并不多,见下表。
工具 | 平台 | 是否开源 |
---|---|---|
Coverage Validator | windows | 商用 |
OpenCppCoverage | windows | 开源 (只支持VS2013以上版本) |
gcov + lcov | linux | 开源 |
测试覆盖率工具一般安装部署在持续集成对应的机器上,这样每次持续集成服务器跑完测试用例后,就会根据当前的测试运行情况自动计算出所有代码的测试覆盖率结果,可以详细看到每一行代码的的覆盖情况。生成的报表可以自动发布成一个网页,项目中的所有人都可以看到。
前面我们介绍了单元测试的工具和实施过程,接下来我们看看做好单元测试要注意的一些事项。
在实践的过程中,发现经常有团队虽然开发了大量的单元测试,但是单元测试有效性却很低,付出了大量成本却并没有得到单元测试的收益。总结之后主要有以下一些原因:
异常测试覆盖不足;我们不需要对被测对象的所有可能输入都做测试,但是需要对其做等价类划分,对于每种等价类至少需要一条测试。常见的错误做法是永远只测试正常场景,对异常场景测试的很少。
测试缺少断言;每个测试结束后需要用断言来设置正确的预期结果。如果断言没有写全,那么必然遗漏了重要的检查点,就相当于给安全网撕了个口子。见过一些极致的场景,开发人员为了完成测试用例指标而去凑测试用例数,所有用例不加断言。这样虽然看到执行通过的测试用例很多,测试覆盖率也很好,但是全是无效用例。
测试设计能力不足,测试覆盖没有规划。理想的情况下应该每个测试对被测代码的覆盖是正交的,每个测试用例覆盖产品代码的一部分,整体上防护全部。这需要有顶层的测试设计,尤其是对于后补的单元测试,顶层的测试设计可以规划优先级和从重点区域开始覆盖。常见的误区是开发人员各自加单元测试,但是遗漏了对重要区域的覆盖。
产品代码设计问题,物理或者逻辑依赖太复杂,导致单元测试很难写。这时需要对原有代码边重构边补充单元测试。所以说单元测试能否搞好,不仅仅是测试的问题,即使不采用TDD的方式也得对产品代码中的不合理设计做优化,才能让单元测试更有效。
从长期来看,降低单元测试的成本并不在于使用了更好的单元测试工具,而在于降低由于被测代码改动导致单元测试随之变动的频度。软件之所以和硬件不同就在于它的软,它的存在价值就是为了应对变化。而软件的变化性往往越向内传递越剧烈,这导致了软件单元级别的设计经常处于变更的核心旋涡之中。所以经常见到有些软件团队,一旦需求变化快工期紧,很快就把单元测试抛弃掉了。被测代码变化导致单元测试跟着变化不可能不发生,但是我们要通过设计降低这种联动变化的概率,这样才能降低单元测试的维护成本。
要让单元测试能够以较低成本维护,需要注意一下事项:
由上可见,做好单元测试不只是掌握单元测试工具的使用就万事大吉了。需要对开发人员的能力进行提升,主要包括:
单元测试只是软件测试策略中的一个环节,其它的还有系统测试,集成测试,组件测试等。每一级的测试都有其价值和不足,所以整体测试策略需要关注如何把这些测试策略整合起来,让整体的成本收益率最好。所以从根本上需要站在全局规划整体的测试策略,这块可以参考敏捷测试的测试象限和金字塔模型理论,然后根据项目的实际情况制定合理的整体测试策略。