面向 C++ 的测试驱动开发

jszy 2015-08-07

用测试的方法驱动开发,这个概念的提出已经很长时间了,但测试驱动开发在 C 和 C++的应用和实践却比较晚,本文用一个简单项目的实例说明如何在 C 和 C++的开发过程中,应用测试驱动开发的理念,从需求定义,代码测试案例设计到开发实现这些案例定义的需求,展现了测试驱动开发的魅力。测试驱动开发和现在流行敏捷开发的是分不开的,测试驱动开发是敏捷开发的一个强有力工具,可以帮助我们从简单的设计开始,逐步地有保护重构设计直至完善设计的过程。

测试驱动开发(TDD)背景及综述

测试驱动开发是 Kent 提出的一种新的软件开发流程,现在已广为人知,这种开发方法依赖于极短重复的开发周期,面对开发需求,开发人员要先开发代码测试用例,这些代码实现的测试用例定义了工程要实现的需求,然后去开发代码快速测试通过这这些用例,这个时候的代码是相对比较粗糙的,只是为了通过这个测试,测试通过以后,这些测试所覆盖的需求就会相对固定下来了,然后随着实现更多的需求,以前实现的那些粗糙的代码的问题会逐步的暴露出来,此时就要用重构来消除重复改进代码设计,因为自动化的测试用例已经框定了相应的需求,这样在代码改进和重构的过程中就不会破坏已实现的需求,实现了安全重构。

从测试驱动开发的流程可以看出来,测试驱动开发仅仅要求一个简单的设计开始实现需求,然后随着软件开发的推进实现有保护重构代码和设计。依赖于 TDD 开发所生成的单元测试用例代码,实现有保护重构是大型的软件开发项目不可以缺少的,代码级别的测试更能有效地提高软件产品的质量。测试驱动开发中的重构过程也是一个使设计逐步完善的过程。 本文的主要目的是使测试驱动开发落到实地,和具体的语言(C++)和单元测试框架结合起来,并用实例展示测试驱动开发的魅力。

测试驱动开发的信条

先开发和设计测试代码,再代码实现通过测试,以测试驱动设计实现,开发和设计的过程,得到了快速的反馈,用这些反馈驱动,改进和重构代码设计,是一个有机的开发过程。按照 Kent 的定义,测试驱动开发的原则是:

  1. 不要写一行代码,除非有一个失败的自动化测试案例要纠正。
  2. 消除重复的代码,改进设计。

这两个简单的原则,却产生了一些复杂的个体和组的行为,这些隐含的技术行为包括:

  • 运行代码对设计决定快速反馈下,实现有机地设计
  • 必须自己写自己的测试用例,而不是等待别人帮你写测试代码,那样会花费很长时间
  • 必须要有对变更代码快发反应开发环境
  • 组件必须要高内聚、低耦合,以使测试简单化。

两个原则还隐含开发任务的顺序:

  1. 红色(Red):写些不能够工作的小测试,这个测试甚至不能编译通过。
  2. 绿色(Green):快速编写代码使测试通过,不用太在意代码质量只是通过测试。
  3. 重构(Refactor):消除开始是只是要通过测试的重复代码,改进设计。

红色(Red)-绿色(Green)-重构(Refactor),这个就是测试驱动开发的座右铭(Mantra)。这种开发方式可以有效的减少代码的缺陷密度,减少 bug 的数量,将大部分的缺陷在代码的开发过程中消除,减少了 QA 测试和质量保证的成本。

按照软件工程的说法,软件缺陷和 bug 发现的越早,所需的更正这些缺陷的成本就会越小。所以在软件的开发阶段,采用测试驱动的开发方法,把测试引入到开发阶段,使测试和质量意识融入到开发的过程中,这对提高软件工程质量非常有帮助。 而且在采用测试驱动开发必然要求所开发的组件、接口、类或方法是可测试的(testable),这就要求开发的组件,接口要遵循组件和类高内聚(Highly Cohesive),组件和组件、类和类之间低耦合(loosely Coupled)原则,这种开发方式生成的代码必然会帮助开发者,在不断的有保护重构的过程中,提高软件架构的设计,使日后的软件维护变得有章可循。

测试驱动开发符合敏捷软件开发的精神,在不断迭代过程中,增量地实现软件需求而这一切开始可以从简单设计开始。

单元测试框架比较和筛选

C++技术是一种高级语言,它出现的时间要比 Java 和 C#早得多,但支持像 xUnit 框架的 C++单元测试框架发展起来的比较晚。 C++ 的单元测试框架选择比较多,现在比较流行的 C++测试框架有 Boost Test、UnitTest++、CppTest、Google C++ Testing Framework。 Boost Test,拥有良好的断言功能,对异常控制,崩溃控制方面处理的比较好,也有良好的可以移植性,但结构复杂,不易于掌握。CPPUnit 是开发比较早的单元测试框架,是对 JUnit 的 C++的移植的一种尝试,拥有丰富的断言和期望功能。Google Test C++ 简称 Gtest,是近期发展起来的单元测试框架,对 xUnit 支持的比较好,支持 TDD 的红-绿-重构模式,支持死亡和退出测试,较好的异常测试控制能力,良好的测试报告输出,拥有自动注册测试用例和用例分组等功能,还有和 Gmock 框架的无缝结合,支持基于接口的(抽象类的)Mock 测试-模拟测试。

下表是一个对三种流行 C++单元测试框架的简单比较,Gtest 虽然发展起来的较晚,但丰富功能简单易用,易学,加之移植性较好,是跨平台项目单元测试框架比较好的选择。

表 1.单元测试框架比较
测试框架支持特性GtestBoost TestCPPUnit
可移植性较好好(依赖于 Boost 库)较好
丰富的断言一般
丰富的断言信息良好较差
自动检测和注册测试用例一般
易于扩展断言易于扩展一般一般
支持死亡和退出测试(Death 和 Exit)支持支持不支持
支持参数化测试(Parameterized test)支持支持不支持
支持 Scoped_Trace支持不支持不支持
支持选择性执行测试用例支持支持支持
丰富的测试报告形式(xml)支持支持支持
支持测试用例分组 Suites支持支持支持
开源
执行速度
基于接口的Mock测试通过Gmock支持不支持不支持
易用性优秀较复杂较好
支持类型化的参数化测试支持不直接支持不直接支持

测试驱动开发-GTest 简介

Gtest 是基于 xUnit 的 C++单元测试框架,支持自动化案例自动发掘,丰富的断言功能,支持用户自定义断言,支持死亡测试和退出测试,还有异常测试控制,支持值类型和类型化的参数化测试,接口简单易用,对每个测试案例有执行时间的输出,可以帮助分析代码的执行效率,单一接口文件 gtest.h。

图 1 是 Console 模式输出用红和绿表示失败和成功的测试用例,看起来比较符合 TDD 的策略和定义

图 1.GTest 的案例测试结果输出

面向 C++ 的测试驱动开发

Gtest 的断言有两种形式,致命性断言(Fatal Assertion)和非致命性断言(Nonfatal Assertion)。

除了基本的断言形式外,Gtest 还包括一些其他的高级断言形式,比如死亡断言,退出断言测试和异常断言等。

Gtest 还有其他的一些特性,比如类型参数化测试,值类型参数化的测试,测试用例分组,洗牌式测试等,可以参照附录中列出的 Gtest 的官网获取更多的信息。

在测试驱动软件开发的过程中,我们不可避免的要去依赖第三方系统,比如文件系统、第三方库、数据库访问,其他的在线数据的访问等,按照测试驱动开发的快速反馈的原则,如果在单元测试用例中去直接访问这些信息,势必在测试驱动开发过程中会依赖这些资源从而造成访问时间无法控制, 所以单元测试一般应该避免直接访问第三方系统,这就是 Mock 测试的主要目的,用模拟的接口去替换真实的接口,模拟出单元测试需要的第三方数据和接口进而隔离第三方的影响,专注于自己的逻辑实现。Gmock 就是这样一个 Mock 框架,它是类似于 jMock、EasyMock 和 Hamcres ,但是是 C++版本的 Mock 框架。 Gmock 是基于接口的 Mock 框架,在 C++中接口的定义是通过抽象函数和抽象类来实现的,这种要求势必会要求我们尽量遵循基于接口的编程原则,把交互界面上的操作抽象成接口,以便是接口可被模拟 Mock。可以在附录中列出的 Gmock 官网获取更多信息。 

相关推荐