CppUnit 是个基于 LGPL 的开源项目,初版本移植自 JUnit,是一个非常的开源测试框架。CppUnit 和 JUnit 一样主要思想来源于极限编程(XProgramming)。主要功能是对单元测试进行管理,并可进行自动化测试。这样描述可能没有让您体会到测试框架的强大威力,那您在开发过程中遇到下列问题吗?如果答案是肯定的,应该学习使用这种技术:
测试代码没有很好地维护而废弃,再次需要测试时还需要重写;
投入太多的精力,找 bug,而新的代码仍然会出现类似 bug;
写完代码,心里没底,是否有大量 bug 等待自己;
新修改的代码不知道是否影响其他部分代码;
由于牵扯太多,导致不敢进行修改代码;
...
这些问题下文都会涉及。这个功能强大的测试框架在国内的 C++ 语言开发人员中使用的不是很多。本文介绍这个框架,希望能够用少的代价尽快掌握这种技术。下面从基本原理,CppUnit 原理,手动使用步骤,通常使用步骤,其他实际问题等方面进行讨论。以下讨论基于 CppUnit1.8.0。
1. 基本原理
对于上面的问题仅仅说明 CppUnit 的使用是没有效果的,下面先从测试的目的,测试原则等方面简要说明,然后介绍 CppUnit 的具体使用。
首先要明确我们写测试代码的目的,是验证代码的正确性或者调试 bug。这样写测试代码时有了针对性,对那些容易出错的,易变的编写测试代码;而不用对每个细节,每个功能编写测试代码,当然除非有过量精力或者可靠性要求。
编码和测试的关系是密不可分的,推荐的开发过程并不要等编写完所有或者很多的代码后再进行测试,而是在完成一部分代码,比如一个函数,之后立刻编写测试代码进行验证。然后再写一些代码,再写测试。每次测试对所有以前的测试都进行一遍。这样做的优点是,写完代码,也基本测试完一遍,心里对代码有信心。而且在写新代码时不断地测试老代码,对其他部分代码的影响能够迅速发现、定位。不断编码测试的过程也是对测试代码维护的过程,以便测试代码一直是有效的。有了各个部分测试代码的保证,有了自动测试的机制,更改以前的代码没有什么顾虑了。在极限编程(一种软件开发思想)中,甚至强调先写测试代码,然后编写符合测试代码的代码,进而完成整个软件。
根据上面说的目的、思想,下面总结一下平时开发过程中单元测试的原则:
先写测试代码,然后编写符合测试的代码。至少做到完成部分代码后,完成对应的测试代码;
测试代码不需要覆盖所有的细节,但应该对所有主要的功能和可能出错的地方有相应的测试用例;
发现 bug,首先编写对应的测试用例,然后进行调试;
不断总结出现 bug 的原因,对其他代码编写相应测试用例;
每次编写完成代码,运行所有以前的测试用例,验证对以前代码影响,把这种影响尽早消除;
不断维护测试代码,保证代码变动后通过所有测试;
有上面的理论做指导,测试行为可以有规可循。那么 CppUnit 如何实现这种测试框架,帮助我们管理测试代码,完成自动测试的?下面看看 CppUnit 的原理。
2. CppUnit 的原理
在 CppUnit 中,一个或一组测试用例的测试对象被称为 Fixture(设施,下文为方便理解尽量使用英文名称)。Fixture 是被测试的目标,可能是一个对象或者一组相关的对象,甚至一个函数。
有了被测试的 fixture,可以对这个 fixture 的某个功能、某个可能出错的流程编写测试代码,这样对某个方面完整的测试被称为TestCase(测试用例)。通常写一个 TestCase 的步骤包括:
对 fixture 进行初始化,及其他初始化操作,比如:生成一组被测试的对象,初始化值;
按照要测试的某个功能或者某个流程对 fixture 进行操作;
验证结果是否正确;
对 fixture 的及其他的资源释放等清理工作。
对 fixture 的多个测试用例,通常(1)(4)部分代码都是相似的,CppUnit 在很多地方引入了 setUp 和 tearDown 虚函数。可以在 setUp 函数里完成(1)初始化代码,而在 tearDown 函数中完成(4)代码。具体测试用例函数中只需要完成(2)(3)部分代码即可,运行时 CppUnit 会自动为每个测试用例函数运行 setUp,之后运行 tearDown,这样测试用例之间没有交叉影响。
对 fixture 的所有测试用例可以被封装在一个 CppUnit::TestFixture 的子类(命名惯例是[ClassName]Test)中。然后定义这个fixture 的 setUp 和 tearDown 函数,为每个测试用例定义一个测试函数(命名惯例是 testXXX)。下面是个简单的例子:
class MathTest : public CppUnit::TestFixture {
protected:
int m_value1, m_value2;
public:
MathTest() {}
// 初始化函数
void setUp () {
m_value1 = 2;
m_value2 = 3;
}
// 测试加法的测试函数
void testAdd () {
// 步骤(2),对 fixture 进行操作
int result = m_value1 + m_value2;
// 步骤(3),验证结果是否争取
CPPUNIT_ASSERT( result == 5 );
}
// 没有什么清理工作没有定义 tearDown.
}
在测试函数中对执行结果的验证成功或者失败直接反应这个测试用例的成功和失败。CppUnit 提供了多种验证成功失败的方式:
CPPUNIT_ASSERT(condition) // 确信condition为真
CPPUNIT_ASSERT_MESSAGE(message, condition) // 当condition为假时失败, 并打印message
CPPUNIT_FAIL(message) // 当前测试失败, 并打印message
CPPUNIT_ASSERT_EQUAL(expected, actual) // 确信两者相等
CPPUNIT_ASSERT_EQUAL_MESSAGE(message, expected, actual) // 失败的同时打印message
CPPUNIT_ASSERT_DOUBLES_EQUAL(expected, actual, delta) // 当expected和actual之间差大于delta时失败
要把对 fixture 的一个测试函数转变成一个测试用例,需要生成一个 CppUnit::TestCaller 对象。而终运行整个应用程序的测试代码的时候,可能需要同时运行对一个 fixture 的多个测试函数,甚至多个 fixture 的测试用例。CppUnit 中把这种同时运行的测试案例的集合称为 TestSuite。而 TestRunner 则运行测试用例或者 TestSuite,具体管理所有测试用例的生命周期。目前提供了 3 类TestRunner,包括:
CppUnit::TextUi::TestRunner // 文本方式的TestRunner
CppUnit::QtUi::TestRunner // QT方式的TestRunner
CppUnit::MfcUi::TestRunner // MFC方式的TestRunner
下面是个文本方式 TestRunner 的例子:
CppUnit::TextUi::TestRunner runner;
CppUnit::TestSuite *suite= new CppUnit::TestSuite();
// 添加一个测试用例
suite->addTest(new CppUnit::TestCaller<MathTest> (
"testAdd", testAdd));
// 指定运行TestSuite
runner.addTest( suite );
// 开始运行, 自动显示测试进度和测试结果
runner.run( "", true ); // Run all tests and wait
对测试结果的管理、显示等功能涉及到另一类对象,主要用于内部对测试结果、进度的管理,以及进度和结果的显示。这里不做介绍。
下面我们整理一下思路,结合一个简单的例子,把上面说的思路串在一起。