单元测试是一段能够放在批处理中自动运行的,用来测试源代码的程序。单元测试测试一小段代码或一个足够小的功能。单元测试程序调用这小段代码或功能,并验证返回的结果是否符合预先设定的结果。
一般地,要由开发人员负责编写和维护测试单元。单元测试通常用来测试如下情况:边界、异常的数据类型、组件之间的接口,以及在软件改变时需要不断检验的复杂操作。单元测试应当在软件构建时定期运行。
CppUnit 是个基于 LGPL 的开源项目,最初版本移植自 JUnit,是一个非常优秀的开源测试框架。CppUnit 和 JUnit 一样主要思想来源于极限编程(XProgramming)。主要功能就是对单元测试进行管理,并可进行自动化测试。这样描述可能没有让您体会到测试框架的强大威力,那您在开发过程中遇到下列问题吗?如果答案是肯定的,就应该学习使用这种技术:
l 测试代码没有很好地维护而废弃,再次需要测试时还需要重写;
l 投入太多的精力,找 bug,而新的代码仍然会出现类似 bug;
l 写完代码,心里没底,是否有大量 bug 等待自己;
l 新修改的代码不知道是否影响其他部分代码;
l 由于牵扯太多,导致不敢进行修改代码;
l ...
首先要明确我们写测试代码的目的,就是验证代码的正确性或者调试 bug。这样写测试代码时就有了针对性,对那些容易出错的,易变的编写测试代码;而不用对每个细节,每个功能编写测试代码,当然除非有过量精力或者可靠性要求。
编 码和测试的关系是密不可分的,推荐的开发过程并不要等编写完所有或者很多的代码后再进行测试,而是在完成一部分代码,比如一个函数,之后立刻编写测试代码 进行验证。然后再写一些代码,再写测试。每次测试对所有以前的测试都进行一遍。这样做的优点就是,写完代码,也基本测试完一遍,心里对代码有信心。而且在 写新代码时不断地测试老代码,对其他部分代码的影响能够迅速发现、定位。不断编码测试的过程也就是对测试代码维护的过程,以便测试代码一直是有效的。有了 各个部分测试代码的保证,有了自动测试的机制,更改以前的代码没有什么顾虑了。在极限编程(一种软件开发思想)中,甚至强调先写测试代码,然后编写符合测 试代码的代码,进而完成整个软件。
根据上面说的目的、思想,下面总结一下平时开发过程中单元测试的原则:
有上面的理论做指导,测试行为就可以有规可循。那么 CppUnit 如何实现这种测试框架,帮助我们管理测试代码,完成自动测试的?下面就看看 CppUnit 的原理。
在 CppUnit 中,一个或一组测试用例的测试对象被称为 Fixture(设施,下文为方便理解尽量使用英文名称)。Fixture 就是被测试的目标,可能是一个对象或者一组相关的对象,甚至一个函数。
有了被测试的 fixture,就可以对这个 fixture 的某个功能、某个可能出错的流程编写测试代码,这样对某个方面完整的测试被称为TestCase(测试用例)。通常写一个 TestCase 的步骤包括:
对 fixture 的多个测试用例,通常(1)(4)部分代码都是相似的,CppUnit 在很多地方引入了 setUp 和 tearDown 虚函数。可以在 setUp 函数里完成(1)初始化代码,而在 tearDown 函数中完成(4)代码。具体测试用例函数中只需要完成(2)(3)部分代码即可,运行时 CppUnit 会自动为每个测试用例函数运行 setUp,之后运行 tearDown,这样测试用例之间就没有交叉影响。
对 fixture 的所有测试用例可以被封装在一个 TestCase 的子类(命名惯例是[ClassName]TestCase)中。然后定义这个fixture 的 setUp 和 tearDown 函数,为每个测试用例定义一个测试函数(命名惯例是 testXXX)。下面是个简单的例子:
class MathTestCase : public TestCase { protected:
int m_value1; int m_value2;
public: MathTestCase (std::string name) : TestCase (name) {}
void setUp (); static Test *suite ();
protected: void testAdd (); };
void MathTestCase::setUp ()// 初始化 { m_value1 = 2; m_value2 = 3; }
void MathTestCase::testAdd ()// 测试加法的测试函数 { // 步骤(2),对 fixture 进行操作 double result = m_value1 + m_value2; // 步骤(3),验证结果是否争取 assert (result == 6.0); }
// 没有什么清理工作没有定义 tearDown. |
|
在测试函数中对执行结果的验证成功或者失败直接反应这个测试用例的成功和失败。CppUnit 提供了多种验证成功失败的方式:
assert(condition) // 确信condition为真 // 判断double型expected值和actual值是否相等,delta为允许偏差 assertDoublesEqual (expected,actual,delta) // 判断长整形expected值和actual值是否相等 assertLongsEqual (expected,actual) // 当前测试失败, 并打印message
|
|
最终运行整个应用程序的测试代码的时候,可能需要同时运行对一个 fixture 的多个测试函数,甚至多个 fixture 的测试用例。CppUnit 中把这种同时运行的测试案例的集合称为 TestSuite。而 TestRunner 则运行测试用例或者 TestSuite,具体管理所有测试用例的生命周期。
下面是个TestRunner 的例子:
TestSuite *testSuite = new TestSuite ("MathTestCase");
// 添加一个测试用例 testSuite->addTest (new TestCaller ( "testAdd", &MathTestCase::testAdd)); // 指定运行TestSuite runner.addTest( testSuite ); // 开始运行, 自动显示测试进度和测试结果 |
CPPUnit使用标准C++开发,可以应用到Windows、Linux等多种操作平台,也可以与Visual C++和C++ Builder等IDE集成使用。下面就介绍一下如何在C++ Builder中使用CPPUnit。
在CPPUnit 的官方网站上只提供了针对Visual C++的DLL,并有详细的指导手册,网上的文章大多数也是介绍如何在VC中集成CPPUnit的。相比下C++ Builder就被冷落了许多,好在还是有C++ Builder爱好者不甘落后,提供了能用于C++ Builder的CppUnit GUI。
从网上下载CppUnit17BCB30Pro.zip这个文件,将文件展开,目录如下:
其中CppUnit15ProjectGroup.bpg 为工程组文件,test目录包含测试框架,borland下的工程提供图形化的界面显示测试用例及测试结果,samples目录包含示例程序。这个工程是 用C++ Builder3创建的,所包含的测试框架也是CppUnit1.5(目前CppUnit的最新版本为1.9),可能有点落伍了。管它呢,够用就 好...。下面就针对这个版本,结合一个简单的例子,介绍如何创建自己的测试用例。
在borland 子目录有一个HostApp子目录,里面包含HostApp工程,该工程就是我们的测试用例的宿主,我们将往该工程添加测试用例。在添加用例之前,首先需 要明确测试的对象 fixture,然后根据其功能、流程,以及以前的经验,确定测试用例。这个步骤非常重要,直接关系到测试的最终效果。当然增加测试用例的过程是个阶段性 的工作,开始完成代码后,先完成对功能的测试用例,保证其完成功能;然后对可能出错的部分,结合以前的经验(比如边界值测试、路径覆盖测试等)编写测试用 例;最后在发现相关 bug 时,根据 bug 完成测试用例。
比如对整数加法进行测试,首先定义一个新的 TestCase 子类,MathTestCase,编写测试用例的测试代码。后期需要添加新的测试用例时只需要添加新的测试函数,根据需要修改 setUp 和 tearDown 即可。如果需要对新的 fixture 进行测试,定义新的 TestCase 子类即可。
把需要测试的测试用例组织到 TestSuite 中,然后通过 TestRuner 运行。这部分代码后期添加新的测试用例时需要改动的不多。只需要把新的测试用例添加到 TestSuite 中即可。具体方法是,打开HostAppUnitForm.cpp文件,在startTesting()方法的runner.run();语句前添加如下 代码:
TestSuite *testSuite = new TestSuite ("MathTestCase"); testSuite->addTest (new TestCaller ("testAdd", &MathTestCase::testAdd)); runner.addTest( testSuite ); |
运行HostApp.exe,这时系统会出现以下界面:
树型节点上会列出所有的TestSuite及TestCase,选择其中一个TestSuite或TestCase,然后点击Run按钮,即可启动测试用例,下面会显示运行的测试用例数、失败的用例数及错误数。最下面的StatusBar还会显示测试用例的执行时间。如果没有测试用例失败,进度条就会用绿色表示,如图所示:
如果测试失败,进度条就是红色的,在下面的列表还会列出错误类型、测试用例名、失败用例的条件、失败语句位于文件的行号及文件名。
通常包含测试用例代码和被测试对象是在不同的项目中。应该在另一个项目(最好在不同的目录)中编写 TestCase,然后把被测试的对象包含在测试项目中。
[参考]
· 李群, 《便利的开发工具 CppUnit 快速使用指南》
· CppUnit 主页
· CppUnit Cookbook
· xprogramming 主页
· Martin Fowler ,《重构-改善既有代码的设计》