简介
测试是软件开发过程中极其重要的一环,详尽周密的测试能够减少软件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的简单介绍就到此,相信你已经了解了其中的基本概念,也能够开发单元测试代码了。