摘要
测试驱动开发(TDD)是以测试作为开发过程的中心,它坚持在编写实际代码之前,先写好基于产品代码的测试代码。开发过程的目标就是首先使测试能够通过,然后再优化设计结构。XUnit是一个基于测试驱动开发的测试框架,它为我们在开发过程中使用测试驱动开发提供了一个方便的工具,使我们得以快速的进行单元测试。XUnit的成员有很多,如JUnit,PythonUnit等。今天论文中讨论的CppUnit 即是XUnit家族中的一员,它是一个专门面向C++的测试框架。
一、引言
CppUnit是基于LGPL的开源项目,最初版本移植自JUnit,是个非常优秀开源测试框架。CppUnit和JUnit样主要思想来源于编程,主要功能是对单元测试进行管理并可进行自动化测试,CppUnit设计模式代码也相对好理解。实验使用的CppUnit的最新版本1.12.1,开发环境为WindowsXp的VC6.0。
本文不对CppUnit源码做详细的介绍,而只是对CppUnit在VC6.0环境下的应用作一些介绍。文章的安排如下:第二部分介绍CppUnit源代码的组成;第三部分介绍CppUnit的基本框架和概念;第四部分说明CppUnit的安装与配制,在VC中的使用,讨论怎样为产品代码添加测试代码(实际上应该反过来,为测试代码添加产品代码。在TDD中,先有测试代码后有产品代码),并通过CppUnit来进行测试;第五部分列出了实验的测试结果。
二、CppUnit源代码的组成
CppUnit是开源产品,从http://sourceforge.net/projects/cppunit 下载源码包,当前最高版本为1.12.1。下载后,将源码包解压缩到本地硬盘,例如解压到D: cppunit-1.12.1。下载解压后,你将看到如下文件夹,如图1:
图1 CppUnit源代码的组成
主要的文件夹有:
doc: CppUnit的说明文档。另外,代码的根目录,还有三个说明文档,分别是INSTALL,INSTALL-unix,INSTALL-WIN32.txt;
examples: CpppUnit提供的例子,也是对CppUnit自身的测试,通过它可以学习如何使用CppUnit测试框架进行开发;
include: CppUnit头文件;
src: CppUnit源代码目录;
config:配置文件;
contrib:contribution,其他人贡献的外围代码;
lib:存放编译好的库;
src:源文件,以及编译库的project等;
接下来进行编译工作。 在src/目录下, 将CppUnitLibraries.dsw工程文件用vc 打开。执行build/batch build,编译成功的话,生成的库文件将被拷贝到lib目录下。中途或者会有些project编译失败,一般不用管它,我们重点看的是cppunit和 TestRunner 这两个project的debug和release版本。
编译通过以后, 在lib/目录下,会生成若干lib,和dll文件, 都以cppunit开头. cppunitd表示debug版, cppunit表示release版。
CppUnit为我们提供了两套框架库,一个为静态的lib,一个为动态的dll。cppunit project:静态lib;cppunit_dll project:动态dll和lib。在开发中我们可以根据实际情况作出选择。可以根据需要选择所需的项目进行编译,其中项目cppunit为静态库,cppunit_dll为动态库,生成的库文件为:
cppunit.lib:静态库release版;
cppunitd.lib:静态库debug版;
cppunit_dll.lib:动态库release版;
cppunitd_dll.lib:动态库debug版;
另外一个很重要的project是TestRunner,它输出一个dll,提供了一个基于GUI 方式的测试环境,在CppUnit下, 可以选择控制台方式和GUI方式两种表现方案。两种方案分别如下图所示,图2为GUI方式,图3为文本方式:
图2 GUI方式
图3 控制台方式
要使用CppUnit,还得设置好头文件和库文件路径,以VC6.0为例,选择Tools/Options/Directories,在Include files和Library files中分别添加文件所在的路径。本文这里分别填的是D:\CPPUNIT-1.12.1\INCLUDE和D:\CPPUNIT-1.12.1\LIB。如图4所示:
图4 包含相应的文件
三、CppUnit的基本框架和概念
CppUnit的基本框架与JUnit类似,核心内容主要包括一些关键类,如Test类、TestFixture类、TestCase类、TestSuite类、TestFactory类及TestRunner类。
1、Test:所有测试对象的基类。
CppUnit采用树形 结构来组织管理测试对象(类似于目录树,如下图所示),因此这里采用了组合设计模式(Composite Pattern),Test的两个直接子类TestLeaf和TestComposite分别表示“测试树”中的叶节点和非叶节点,其中 TestComposite主要起组织管理的作用,就像目录树中的文件夹,而TestLeaf才是最终具有执行能力的测试对象,就像目录树中的文件。
Test最重要的一个公共接口run,定义为virtual void run(TestResult *result) = 0;其作用为执行测试对象,将结果提交给result。
在实际应用中,我们一般不会直接使用Test、TestComposite以及TestLeaf,除非我们要重新定制某些机制。
2、TestFixture:用于维护一组测试用例的上下文环境。
在实际应用中,我们经常会开发一组测试用例来对某个类的接口加以测试,而这些测试用例很可能具有相同的初始化和清理代码。为此,CppUnit引入TestFixture来实现这一机制。
TestFixture具有以下两个接口,分别用于处理测试环境的初始化与清理工作:
virtual void setUp();
virtual void tearDown();
3、TestCase:测试用例,从名字上就可以看出来,它便是单元测试的执行对象。
TestCase从Test和TestFixture多继承而来,通过把Test::run制定成模板函数(Template Method)而将两个父类的操作融合在一起,用户需从TestCase派生出子类并实现runTest以开发自己所需的测试用例。另外还有一个重要的就是TestResult的protect方法,其作用是对执行函数(实际上是函数对象)的错误信息(包括断言和异常等)进行捕获,从而实现对测试结果的统计。
4、TestSuite:测试包,按照树形结构管理测试用例
TestSuit是TestComposite的一个实现,它采用vector来管理子测试对象(Test),从而形成递归的树形结构。
5、TestFactory:测试工厂
这是一个辅助类,通过借助一系列宏定义让测试用例的组织管理变得自动化。可以参见第三部分的例子。
6、TestRunner:用于执行测试用例
TestRunner将待执行的测试对象管理起来,然后供用户调用。其接口为
virtual void addTest( Test *test );
virtual void run( TestResult &controller, const std::string &testPath = "" );
这也是一个辅助类,需注意的是,通过addTest添加到TestRunner中的测试对象必须是通过new动态创建的,用户不能删除这个对象,因为TestRunner将自行管理测试对象的生命期。
四、CppUnit的使用
1、CppUnit的配制
了解了以上的知道就可以正式使用CppUnit了,由于单元测试是TDD(测试驱动开发)的利器,一般人会先写测试代码,然后再写产品代码。CppUnit最小的测试单位是TestCase,多个相关TestCase组成一个TestSuite。要添加测试代码最简单的方法就是利用CppUnit为我们提供的几个宏来进行(当然还有其他的手工加入方法,但均是殊途同归,可以查阅CppUnit头文件中的演示代码)。这几个宏是:
CPPUNIT_TEST_SUITE() 开始创建一个TestSuite;
CPPUNIT_TEST() 添加TestCase;
CPPUNIT_TEST_SUITE_END() 结束创建TestSuite;
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION() 添加一个TestSuite到一个指定的TestFactoryRegistry工厂。
假定我们要实现一个类,类名暂且取做Calculator类,它的功能主要是实现两个数加减乘除。 假定这个类要实现了四个方法是:
int add(int nNum1, int nNum2);
int substract(int nNum1, int nNum2);
int multiply(int nNum1, int nNum2);
int divide(int nNum1, int nNum2);
测试驱动开发先写测试代码,后写产品代码(Calculator类),先写的测试代码往往是不能运行或编译的,目标是在写好测试代码后写产品代码,使之编译通过,然后再进行重构。这就是Kent Beck说的“red/green/refactor”。所以,上面的类名和方法应该还只是在程序员的心里,还只是一个idea而已。
根据测试驱动的原理,我们需要先建立一个单元测试框架。在VC中为测试代码建立一个project。通常,测试代码和被测试对象(产品代码)是处于不同的project中的。这样就不会让你的产品代码被测试代码所“污染 ”。
由于在CppUnit下, 可以选择控制台方式和UI方式两种表现方案,本文中选择UI方式。在本例中,我们将建立一个基于GUI 方式的测试环境。因此我们建立一个基于对话框的Project。假设名为UnitTest。建立了UnitTest project之后,首先配置这个工程。
首先在project中打开RTTI开关,具体位置在菜单Project/Settings/C++/C++ Language。如下图所示设置:
图5 打开RTTI
最后应该在project中连接正确的lib。包括本例采用的cppunit.lib和cppunitd.lib静态库以及用于GUI方式的TestRunner.dll对应的lib。具体位置在Project/Settings/Link/General 在‘Object/library modules’中,针对debug和release分别加入cppunitd.lib testrunnerd.lib和cppunit.lib TestRunner.lib。由于TestRunner.dll为我们提供了基于GUI的测试环境。为了让我们的测试程序能正确的调用它,TestRunner.dll必须位于 你的测试程序的路径下。所以把/lib目录下的testrunnerd.dll和TestRunner.dll文件分别拷贝到UnitTest priject的程序debug和release版本输出目录中。
2、程序的测试框架
配置工作后,下面开始写测试框架。在CppUnit中, 是以TestCase为最小的测试单位, 若干TestCase组成一个TestSuite。所以我们要先建立一个TestCase。
在UnitTest project中新建一个类, 命名为CCalTestCase, 让其从CppUnit::TestCase派生。为其新增一个方法,假设为 void testAdd(); 我们将在这个函数中写入我们的一些测试代码。代码如下:
- #include <cppunit/TestCase.h>
- class CCalTestCase : public CppUnit::TestCase
- {
- public:
- CCalTestCase ();
- virtual ~ CCalTestCase ();
- void testAdd();
- };
CPPUNIT_TEST_SUITE();
CPPUNIT_TEST();
CPPUNIT_TEST_SUITE_END();
第一个宏声明一个测试包(suite),第二个宏声明(添加)一个测试用例。 现在我们的CCalTestCase类看上去像这样:切记要包含头文件,否则无法识别这些宏。
- #include <cppunit/TestCase.h>
- #include <CppUnit/extensions/HelperMacros.h>
- class CCalTestCase : public CppUnit::TestCase
- {
- CPPUNIT_TEST_SUITE(CCalTestCase);
- CPPUNIT_TEST(testAdd);
- CPPUNIT_TEST_SUITE_END();
- public:
- CCalTestCase ();
- virtual ~ CCalTestCase ();
- void testAdd();
- };
使用CPPUNIT_TEST_SUITE_NAMED_REGISTRATION()来注册一个测试suite。这个宏的第二个参数是我们注册的suite的名字。在这里我们可以用字符串来代替,但我们用一个静态函数来返回这个suite的名字。
- // CalTestCase.cpp
- std::string
- CCalTestCase::GetSuiteName()
- {
- return " Calculator ";
- }
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(CCalTestCase, CCalTestCase::GetSuiteName());它将CCalTestCase这个TestSuite注册到一个指定的TestFactory工厂中。接下来我们写一个注册函数static CppUnit::Test* GetSuite(), 使其在运行期生成一个Test。
- // CalTestCase.h
- class CCalTestCase : public CppUnit::TestCase
- {
- CPPUNIT_TEST_SUITE(CCalTestCase);
- CPPUNIT_TEST(testAdd);
- CPPUNIT_TEST_SUITE_END();
- public:
- CCalTestCase ();
- virtual ~ CCalTestCase ();
- void testAdd();
- static std::string GetSuiteName();
- static CppUnit::Test* GetSuite();
- };
- // CalTestCase.cpp
- CppUnit::Test* CCalTestCase::GetSuite()
- {
- CppUnit::TestFactoryRegistry& reg =
- CppUnit::TestFactoryRegistry::getRegistry (CCalTestCase::GetSuiteName());
- return reg.makeTest();
- }
#include <cppunit/extensions/TestFactoryRegistry.h>最后, 我们为单元测试建立一个UI测试界面。由于我们希望这个Project运行后显示的是GUI界面,所以我们需要在App的 InitInstance ()中屏蔽掉原有的对话框,代之以CppUnit的GUI。在CUnitTestApp::InitInstance()函数中,将原先显示主对话框的代码以下面的代码取代:
- CppUnit::MfcUi::TestRunner runner;
- runner.addTest(CCalTestCase::GetSuite());//添加测试
- runner.run();//show UI
- /* CUnitTestDlg dlg;
- m_pMainWnd = &dlg;
- int nResponse = dlg.DoModal();
- if (nResponse == IDOK)
- {
- // TODO: Place code here to handle when the dialog is
- // dismissed with OK
- }
- else if (nResponse == IDCANCEL)
- {
- // TODO: Place code here to handle when the dialog is
- // dismissed with Cancel
- }
- */
#include <cppunit/ui/mfc/TestRunner.h>
#include " CalTestCase.h "
到此为止, 我们已经建立好一个简单的单元测试框架。测试框架虽然写好了,但是测试代码仍然为空,产品代码也还没有写。
3、测试代码
如前所述,在测试类中,我们添加了一个测试方法:void testAdd();它测试的对象是前面提到的CCalculator类的方法:int Add(int nNum1, int nNum2);(产品代码)我们来看看testAdd()的实现,在CalTestCase.h中包含头文件#include <cppunit/TestAssert.h>,测试代码如下:
- // CalTestCase.cpp
- void CCalTestCase::testAdd()
- {
- CCalculator cal;
- int nResult = cal.add(10, 20); //执行Add操作
- CPPUNIT_ASSERT_EQUAL(30, nResult); //检查结果是否等于30
- }
- void CCalTestCase::testSub()
- {
- Calculator cal;
- int nResult = cal.substract(10, 20); //执行Substract操作
- CPPUNIT_ASSERT_EQUAL(-10, nResult); //检查结果是否等于-10
- }
- void CCalTestCase::testMul()
- {
- Calculator cal;
- int nResult = cal.multiply(10, 20); //执行Multiply操作
- CPPUNIT_ASSERT_EQUAL(200, nResult); //检查结果是否等于200
- }
- void CCalTestCase::testDiv()
- {
- Calculator cal;
- int nResult = cal.divide(20, 10); //执行Divide操作
- CPPUNIT_ASSERT_EQUAL(2, nResult); //检查结果是否等于2
- }
4、书写产品代码
在VC中建立一个MFC Extension Dll的Project,在这个Project 中加入类Calculator,它的声明如下:
- // Calculator.h
- class Calculator
- {
- public:
- Calculator();
- virtual ~Calculator();
- int add(int n,int m);
- int substract(int n,int m);
- int multiply(int n,int m);
- int divide(int n,int m);
- };
- 实现代码如下:
- // Calculator.cpp
- int Calculator::add(int n,int m)
- {
- return n+m;
- }
- Calculator::substract(int n,int m)
- {
- return n-m;
- }
- Calculator::multiply(int n,int m)
- {
- return n*m;
- }
- Calculator::divide(int n,int m)
- {
- if(m == 0)
- {
- return MAXNUM;
- }
- return n/m;
- }
在包含测试代码的Project(即UnitTest) dependent这个产品代码,并且include 相关头文件 ,Rebuild All,执行得如下结果:
图6 实现的四个测试TestMethod
图7 测试结果
转载自:
http://blog.csdn.net/pengtwelve/article/details/7173254