测试是软件开发过程中极其重要的一环,详尽周密的测试能够减少软件BUG,提高软件品质。测试包括单元测试、系统测试等。其中单元测试是指针对软件功能单元所作的测试,这里的功能单元可以是一个类的属性或者方法,测试的目的是看这些基本单元是否工作正常。由于单元测试的内容很基础,因此可以看作是测试工作的第一环,该项工作一般由开发人员自行完成。如果条件允许,单元测试代码的开发应与程序代码的开发同步进行。

虽然不同程序的单元测试代码不尽相同,但测试代码的框架却非常相似,于是便出现了一些单元测试类库,CppUnit便是其中之一。

CppUnit是XUnit中的一员,XUnit是一个大家族,还包括JUnit和PythonUnit等。CppUnit简单实用,学习和使用起来都很方便,网上已有一些文章对其作介绍,但本文更着重于讲解其中的基本概念和使用方法,以帮助初次接触CppUnit的人员快速入门。

安装

目前,CppUnit的最新版本是1.10.2,你可以从下面地址获取:

http://sourceforge.net/projects/cppunit
解压后,你可以看到CppUnit包含如下目录:

config:  配置文件    contrib: contribution,其他人贡献的外围代码    doc:     文档,需要通过doxygen工具生成,也可以直接从sourceforge站点上下载打包好的文档    examples:示例代码    include: 头文件    lib:     存放编译好的库    src:     源文件,以及编译库的工程等
然后打开src目录下的CppUnitLibraries工程,执行build/batch build,编译成功的话,生成的库文件将被拷贝到lib目录下。

你也可以根据需要选择所需的项目进行编译,其中项目cppunit为静态库,cppunit_dll为动态库,生成的库文件为:

cppunit.lib:     静态库release版    cppunitd.lib:    静态库debug版    cppunit_dll.lib: 动态库release版    cppunitd_dll.lib:动态库debug版
要使用CppUnit,还得设置好头文件和库文件路径,以VC6为例,选择Tools/Options/Directories,在Include files和Library files中分别添加%CppUnitPath%\include和%CppUnitPath%\lib,其中%CppUnitPath%表示CppUnit所在路径。

做好准备工作后,我们就可以编写自己的单元测试代码了。需说明的是,CppUnit所用的动态运行期库均为多线程动态库,因此你的单元测试程序也得使用相应设置,否则会发生冲突。

概念

在使用之前,我们有必要认识一下CppUnit中的主要类,当然你也可以先看后面的例子,遇到问题再回过头来看这一节。

CppUnit核心内容主要包括六个方面,

1. 测试对象(Test,TestFixture,...):用于开发测试用例,以及对测试用例进行组织管理。

2. 测试结果(TestResult):处理测试用例执行结果。TestResult与下面的TestListener采用的是观察者模式(Observer Pattern)。

3. 测试结果监听者(TestListener):TestListener作为TestResult的观察者,担任实际的结果处理角色。

4. 结果输出(Outputter):将结果进行输出,可以制定不同的输出格式。

5. 对象工厂(TestFactory):用于创建测试对象,对测试用例进行自动化管理。

6. 测试执行体(TestRunner):用于运行一个测试。

以上各模块的主要类继承结构如下:

Test              TestFixture      TestResult          TestListener             _______|_________            |                                    |                  |               |            |                           TestSuccessListener    TestComposite   TestLeaf         |                                    |                  |               |____________|                           TestResultCollector              TestSuit                  |                           TestCase                                                   |                      TestCaller<Fixture>                                              Outputter                                    TestFactory                    TestRunner        ____________________|_________________                            |        |                   |                |                   TestFactoryRegistry    CompilerOutputter  TextOutputter    XmlOutputter                      |                                                             TestSuiteFactory<TestCaseType>
接下来再对其中一些关键类作以介绍。

Test:所有测试对象的基类。

CppUnit采用树形结构来组织管理测试对象(类似于目录树),因此这里采用了组合设计模式(Composite Pattern),Test的两个直接子类TestLeaf和TestComposite分别表示“测试树”中的叶节点和非叶节点,其中TestComposite主要起组织管理的作用,就像目录树中的文件夹,而TestLeaf才是最终具有执行能力的测试对象,就像目录树中的文件。

Test最重要的一个公共接口为:

virtual void run(TestResult *result) = 0;
其作用为执行测试对象,将结果提交给result。

在实际应用中,我们一般不会直接使用Test、TestComposite以及TestLeaf,除非我们要重新定制某些机制。

TestFixture:用于维护一组测试用例的上下文环境。

在实际应用中,我们经常会开发一组测试用例来对某个类的接口加以测试,而这些测试用例很可能具有相同的初始化和清理代码。为此,CppUnit引入TestFixture来实现这一机制。

TestFixture具有以下两个接口,分别用于处理测试环境的初始化与清理工作:

virtual void setUp();
virtual void tearDown();
TestCase:测试用例,从名字上就可以看出来,它便是单元测试的执行对象。

TestCase从Test和TestFixture多继承而来,通过把Test::run制定成模板函数(Template Method)而将两个父类的操作融合在一起,run函数的伪定义如下:

// 伪代码
void TestCase::run(TestResult* result)
{
     result->startTest(this); // 通知result测试开始
    if( result->protect(this, &TestCase::setUp) ) // 调用setUp,初始化环境
         result->protect(this, &TestCase::runTest); // 执行runTest,即真正的测试代码
     result->protect(this, &TestCase::tearDown); // 调用tearDown,清理环境
     result->endTest(this); // 通知result测试结束
}
这里要提到的是函数runTest,它是TestCase定义的一个接口,原型如下:

virtual void runTest();
用户需从TestCase派生出子类并实现runTest以开发自己所需的测试用例。

另外还要提到的就是TestResult的protect方法,其作用是对执行函数(实际上是函数对象)的错误信息(包括断言和异常等)进行捕获,从而实现对测试结果的统计。

TestSuit:测试包,按照树形结构管理测试用例

TestSuit是TestComposite的一个实现,它采用vector来管理子测试对象(Test),从而形成递归的树形结构。

TestCaller:TestCase适配器(Adapter),它将成员函数转换成测试用例

虽然我们可以从TestCase派生自己的测试类,但从TestCase类的定义可以看出,它只能支持一个测试用例,这对于测试代码的组织和维护很不方便,尤其是那些有共同上下文环境的一组测试。为此,CppUnit提供了TestCaller以解决这个问题。

TestCaller是一个模板类,它以实现了TestFixture接口的类为模板参数,将目标类中某个符合runTest原型的测试方法适配成TestCase的子类。

在实际应用中,我们大多采用TestFixture和TestCaller相组合的方式,具体例子参见后文。

TestResult和TestListener:处理测试信息和结果

前面已经提到,TestResult和TestListener采用了观察者模式,TestResult维护一个注册表,用于管理向其登记过的TestListener,当TestResult收到测试对象(Test)的测试信息时,再一一分发给它所管辖的TestListener。这一设计有助于实现对同一测试的多种处理方式。

TestFactory:测试工厂

这是一个辅助类,通过借助一系列宏定义让测试用例的组织管理变得自动化。参见后面的例子。

TestRunner:用于执行测试用例

TestRunner将待执行的测试对象管理起来,然后供用户调用。其接口为:

virtual void addTest( Test *test ); virtual void run( TestResult &controller, const std::string &testPath = "" );
这也是一个辅助类,需注意的是,通过addTest添加到TestRunner中的测试对象必须是通过new动态创建的,用户不能删除这个对象,因为TestRunner将自行管理测试对象的生命期。

使用

先让我们看看一个简单的例子:

#include <cppunit/TestCase.h>
#include <cppunit/TestResult.h>
#include <cppunit/TestResultCollector.h>
#include <cppunit/TextOutputter.h>

// 定义测试用例
class SimpleTest : public CppUnit::TestCase
{
public:
    void runTest() // 重载测试方法
     {
        int i = 1;
         CPPUNIT_ASSERT_EQUAL(0, i);
     }
};

int main(int argc, char* argv[])
{
     CppUnit::TestResult r;
     CppUnit::TestResultCollector rc;
     r.addListener(&rc); // 准备好结果收集器

     SimpleTest t;
     t.run(&r); // 运行测试用例

     CppUnit::TextOutputter o(&rc, std::cout);
     o.write(); // 将结果输出

    return 0;
}
编译后运行,输出结果为:
!!!FAILURES!!!
Test Results:
Run: 1 Failures: 1 Errors: 0

1) test: (F) line: 18 E:\CppUnitExamples\SimpleTest.cpp
equality assertion failed
- Expected: 1
- Actual : 0
上面的例子很简单,需说明的是CPPUNIT_ASSERT_EQUAL宏。CppUnit定义了一组宏用于检测错误,CPPUNIT_ASSERT_EQUAL是其中之一,当断言失败时,CppUnit便会将错误信息报告给TestResult。这些宏定义的说明如下:

CPPUNIT_ASSERT(condition):判断condition的值是否为真,如果为假则生成错误信息。

CPPUNIT_ASSERT_MESSAGE(message, condition):与CPPUNIT_ASSERT类似,但结果为假时报告messsage信息。

CPPUNIT_FAIL(message):直接报告messsage错误信息。

CPPUNIT_ASSERT_EQUAL(expected, actual):判断expected和actual的值是否相等,如果不等输出错误信息。

CPPUNIT_ASSERT_EQUAL_MESSAGE(message, expected, actual):与CPPUNIT_ASSERT_EQUAL类似,但断言失败时输出message信息。

CPPUNIT_ASSERT_DOUBLES_EQUAL(expected, actual, delta):判断expected与actual的偏差是否小于delta,用于浮点数比较。

CPPUNIT_ASSERT_THROW(expression, ExceptionType):判断执行表达式expression后是否抛出ExceptionType异常。

CPPUNIT_ASSERT_NO_THROW(expression):断言执行表达式expression后无异常抛出。

接下来再看看TestFixture和TestCaller的组合使用:

#include <cppunit/TestCase.h>
#include <cppunit/TestResult.h>
#include <cppunit/TestResultCollector.h>
#include <cppunit/TextOutputter.h>
#include <cppunit/TestCaller.h>
#include <cppunit/TestRunner.h>

// 定义测试类
class StringTest : public CppUnit::TestFixture
{
public:
    void setUp() // 初始化
     {
         m_str1 = "Hello, world";
         m_str2 = "Hi, cppunit";
     }

    void tearDown() // 清理
     {
     }

    void testSwap() // 测试方法1
     {
        std::string str1 = m_str1;
        std::string str2 = m_str2;
         m_str1.swap(m_str2);
       
         CPPUNIT_ASSERT(m_str1 == str2);
         CPPUNIT_ASSERT(m_str2 == str1);
     }

    void testFind() // 测试方法2
     {
        int pos1 = m_str1.find(',');
        int pos2 = m_str2.rfind(',');

         CPPUNIT_ASSERT_EQUAL(5, pos1);
         CPPUNIT_ASSERT_EQUAL(2, pos2);
     }

protected:
    std::string      m_str1;
    std::string      m_str2;
};

int main(int argc, char* argv[])
{
     CppUnit::TestResult r;
     CppUnit::TestResultCollector rc;
     r.addListener(&rc); // 准备好结果收集器

     CppUnit::TestRunner runner; // 定义执行实体
     runner.addTest(new CppUnit::TestCaller<StringTest>("testSwap", &StringTest::testSwap)); // 构建测试用例1
     runner.addTest(new CppUnit::TestCaller<StringTest>("testFind", &StringTest::testFind)); // 构建测试用例2
     runner.run(r); // 运行测试

     CppUnit::TextOutputter o(&rc, std::cout);
     o.write(); // 将结果输出

    return rc.wasSuccessful() ? 0 : -1;
}
编译后运行结果为:
OK (2 tests)
上面的代码从功能上讲没有什么问题,但编写起来太繁琐了,为此,我们可以借助CppUnit定义的一套辅助宏,将测试用例的定义和注册变得自动化。上面的代码改造后如下:

#include <cppunit/TestResult.h>
#include <cppunit/TestResultCollector.h>
#include <cppunit/TextOutputter.h>
#include <cppunit/TestRunner.h>
#include <cppunit/extensions/HelperMacros.h>


// 定义测试类
class StringTest : public CppUnit::TestFixture
{
     CPPUNIT_TEST_SUITE(StringTest);  // 定义测试包
     CPPUNIT_TEST(testSwap);  // 添加测试用例1
     CPPUNIT_TEST(testFind);  // 添加测试用例2
     CPPUNIT_TEST_SUITE_END();  // 结束测试包定义
   
public:
    void setUp() // 初始化
     {
         m_str1 = "Hello, world";
         m_str2 = "Hi, cppunit";
     }

    void tearDown() // 清理
     {
     }

    void testSwap() // 测试方法1
     {
        std::string str1 = m_str1;
        std::string str2 = m_str2;
         m_str1.swap(m_str2);
       
         CPPUNIT_ASSERT(m_str1 == str2);
         CPPUNIT_ASSERT(m_str2 == str1);
     }

    void testFind() // 测试方法2
     {
        int pos1 = m_str1.find(',');
        int pos2 = m_str2.rfind(',');

         CPPUNIT_ASSERT_EQUAL(5, pos1);
         CPPUNIT_ASSERT_EQUAL(2, pos2);
     }

protected:
    std::string      m_str1;
    std::string      m_str2;
};

CPPUNIT_TEST_SUITE_REGISTRATION(StringTest); // 自动注册测试包

int main(int argc, char* argv[])
{
     CppUnit::TestResult r;
     CppUnit::TestResultCollector rc;
     r.addListener(&rc); // 准备好结果收集器

     CppUnit::TestRunner runner; // 定义执行实体
     runner.addTest(CppUnit::TestFactoryRegistry::getRegistry().makeTest());
     runner.run(r); // 运行测试

     CppUnit::TextOutputter o(&rc, std::cout);
     o.write(); // 将结果输出

    return rc.wasSuccessful() ? 0 : -1;
}
CppUnit的简单介绍就到此,相信你已经了解了其中的基本概念,也能够开发单元测试代码了。