全面了解CPPUnit
一、 单元测试与CPPUNIT简介
几乎每个开发人员都写过测试代码,但是往往这些代码不系统,也没有良好的管理,同时在测试代码编写过程中也有很多重复的劳动,比较繁琐。在一个软件开发过程中,往往会进行很多修改,迭代开发的模型随处可见,如何验证程序的功能、性能和结构是否符合要求是一项重要的工作。
单元测试是属于白盒测试和结构性测试,一般由开发人员开展,当然如果有好的测试工具支持,测试人员甚至最终用户都可以参与。单元测试框架是编写和运行单元测试的软件工具,用来构建测试、运行测试、报告测试结果。对于C/C++开发,比较著名的收费单元测试工具是C++ Test,免费开源的则是CPPUNIT。
CPPUNIT是基于 LGPL 的开源项目,最初版本移植自 JUNIT ,是一个非常优秀的开源测试框架。CPPUNIT和 JUNIT 一样主要思想来源于极限编程。主要功能就是对单元测试进行管理,并可进行自动化测试。CPPUNIT设计遵循很多设计模式,代码结构也相对好理解。
本文将首先讲述CPPUNIT的使用(Linux环境),然后着重深入分析CPPUNIT框架的代码(覆盖了CPPUNIT的所有主干代码),对于想改造CPPUNIT或者程序设计上学习借鉴都起到抛砖引玉的作用。
二、 CPPUNIT安装、使用和框架
1. 安装
CPPUNIT的主页是http://sourceforge.NET/projects/cppunit/,从这里可以获取它的源代码。安装过程也是非常标准的,如下:
Tar zxvf cppunit-1.12.0.tar.gz
cd cppunit-1.12.0
./configure
make
make install
备注:CPPUNIT默认会把头文件和函数库分别安装到/usr/local/include和/usr/local/lib(除非在configure的时候指定prefix),所以编写测试程序的时候需要能搜索/usr/local/include和/usr/local/lib目录,否则会出错。
2. 使用
在CPPUNIT的主目录下有example/simple下的例子代码Main.cpp ExampleTestCase.h/cpp,只要这三个文件即可构造一个CPPUNIT的测试程序。
g++ -o mytest Main.cpp ExampleTestCase.cpp -lcppunit -ldl
一般通过派生TestFixture来编写用户自己的测试类,对于一个实际的测试程序,可能会有很多独立的测试类,因此可以仿照ExampleTestCase.h/cpp编写更多的测试类,而Main.cpp是不用变的,然后在编译(实际上往往写makefile)的时候加上那些源文件即可。
g++ -o mytest Main.cpp MyTestCase1.cpp MyTestCase2.cpp MyTestCase3.cpp ..... -lcppunit -ldl
3. 框架
本节把CPPUNIT的框架分为三个部分进行简单介绍。
1) 测试对象族
CPPUNIT的测试对象的类关系图:
Test:所有测试对象类的抽象基类,主要是定义run方法和统计子对象个数和查找遍历子对象的方法;
TestFixture:该类非常简单,只定义了两个方法setUp和tearDown,作为测试对象的准备和拆除方法,一般用户编写的测试类都直接继承它;
TestComposite、TestLeaf:根据设计模式中组合模式而设计的两个类,都继承自Test;
TestSuite:具体化了TestComposite的内容存储方式、添加子对象接口等等。该类对象包含了若干测试对象,作为测试对象的容器,而且可以嵌套;
TestRunner: 控制测试对象的构造和测试对象执行的类;
TestCase: 定义了一个测试对象要实现的具体接口,同时继承TestFixture的setUp和tearDown;
接口
TestCaller: 使用了设计模式中的策略模式,作为测试对象的最终封装类,提供了测试运行的策略,在测试执行中扮演了重要的角色。它是一个模板类。
2) 信息收集与显示族
CPPUNIT的测试信息收集与显示的类关系图
Outputter:是所有测试输出类的抽象基类,定义了write方法;
CompilerOutputter:以编译器信息类似方式输出测试信息,使用TestResultCollector获取测试信息;
TextOutputter:以文本流的方式输出测试信息,同样使用TestResultCollector获取测试信息;
TestListener:以设计模式中观察者模式定义了Observer所应该具有的从TestResult获取测试步骤信息的方法;
TestSuccessListener:实现了TestListener接口,同时继承了SynchronizedObject了从而具有线程安全性;
SynchronizedObject:该类实现了lock和unlock操作;
ExclusiveZone:使用SynchronizedObject进行了临界区的加锁和解锁操作;
TestResult:这个测试信息的收集者,在观察者模式中扮演Subject角色,是它把测试的各个步骤的信息通知到所有Listener对象的。
3) 测试对象管理族
CPPUNIT测试对象管理类关系图
TestFactory:运用了设计模式中工厂设计模式,这里只定义了一个makeTest方法,是一个抽象基类;
TestSuiteFactory:该类继承自TestFactory,而且是模板类,是生成TestSuite对象的工厂;
TestFactoryRegistry:管理TestFactory对象的类(这里继承自TestFactory个人感觉有点不太恰当);
AutoRegisterSuite:模板类,自动把特定的TestSuiteFactory对象注册到TestFactoryRegistry对象;
TestSuiteBuilderContextBase、TestSuiteBuilderContext:用于构建测试对象的类,详细见代码分析部分。
三、 剖析CPPUNIT
下面以CPPUNIT 1.12.0版本(当前最新版本)源码为对象,把CPPUNIT程序包中的example程序作为剖析对象,这个程序也是我们使用CPPUNIT的经典结构(以下红色的代码片段起强调作用,红色的注释是额外添加的以增强理解)
1. 预编译前C++源代码
int main( int argc, char* argv[] )
{
// Create the event manager and test controller
CPPUNIT_NS::TestResult controller;
// Add a listener that colllects test result
CPPUNIT_NS::TestResultCollector result;
controller.addListener( &result );
// Add a listener that print dots as test run.
CPPUNIT_NS::BriefTestProgressListener progress;
controller.addListener(&progress);
// Add the top suite to the test runner
CPPUNIT_NS::TestRunner runner;
runner.addTest(CPPUNIT_NS::TestFactoryRegistry::getRegistry().makeTest());
runner.run(controller);
// Print test in a compiler compatible format.
CPPUNIT_NS::CompilerOutputter outputter(&result, CPPUNIT_NS::stdCout() );
outputter.write();
return result.wasSuccessful() ? 0 : 1;
}
ExampleTestCase.h
class ExampleTestCase : public CPPUNIT_NS::TestFixture
{
//开始构造一个TestSuite,名字就是ExampleTestCas
CPPUNIT_TEST_SUITE( ExampleTestCase );
//添加一个TestCase(测试方法
CPPUNIT_TEST( example );
//添加一个TestCase(测试方法
CPPUNIT_TEST( anotherExample );
//添加一个TestCase(测试方法)
CPPUNIT_TEST( testAdd );
//添加一个TestCase(测试方法)
CPPUNIT_TEST( testEquals );
//构造TestSuite完毕
CPPUNIT_TEST_SUITE_END();
protected:
double m_value1;
double m_value2;
public:
void setUp();
protected:
void example();
void anotherExample();
void testAdd();
void testEquals();
};
//取出构造的TestSuite加入到全局的管理目录
CPPUNIT_TEST_SUITE_REGISTRATION( ExampleTestCase );
ExampleTestCase.cpp
void ExampleTestCase::example()
{
//CPPUNIT_*开头的是一些断言的宏,如果断言失败则抛出异常
//使用的时候可以参考TestAssert.H头文件
CPPUNIT_ASSERT_DOUBLES_EQUAL( 1.0, 1.1, 0.05 );
CPPUNIT_ASSERT( 1 == 0 );
CPPUNIT_ASSERT( 1 == 1 );
}
void ExampleTestCase::anotherExample()
{
CPPUNIT_ASSERT (1 == 2);
}
void ExampleTestCase::setUp()
{
m_value1 = 2.0;
m_value2 = 3.0;
}
void ExampleTestCase::testAdd()
{
double result = m_value1 + m_value2;
CPPUNIT_ASSERT( result == 6.0 );
}
void ExampleTestCase::testEquals()
{
long* l1 = new long(12);
long* l2 = new long(12);
CPPUNIT_ASSERT_EQUAL( 12, 12 );
CPPUNIT_ASSERT_EQUAL( 12L, 12L );
CPPUNIT_ASSERT_EQUAL( *l1, *l2 );
delete l1;
delete l2;
CPPUNIT_ASSERT( 12L == 12L );
CPPUNIT_ASSERT_EQUAL( 12, 13 );
CPPUNIT_ASSERT_DOUBLES_EQUAL( 12.0, 11.99, 0.5 );
}
2. 预编译后代码
main函数没变化,ExampleTestCase.h/cpp由于包含较多宏,所以变化较大(红色代码是宏展开后的代码),如下所示:
ExampleTestCase.cpp(include "ExampleTestCase.h")
class ExampleTestCase : public CppUnit::TestFixture
{
public:
typedef ExampleTestCase TestFixtureType;
private:
//主要是利用C++的RTTI信息取得用户写的TestFixture类的类名字
static const CppUnit::TestNamer &getTestNamer__()
{
static CppUnit::TestNamer testNamer( typeid(ExampleTestCase) );
return testNamer;
}
public:
typedef CppUnit::TestSuiteBuilderContext
static void addTestsToSuite( CppUnit::TestSuiteBuilderContextBase &baseContext )
{
//把每个测试方法用TestCaller使用具体的TestFixture类型封装起来,加入
//TestSuite中
TestSuiteBuilderContextType context( baseContext );
//生成TestCaller封装测试代码,加入TestSuite
context.addTest( (new CppUnit::TestCaller
context.addTest( ( new CppUnit::TestCaller
context.addTest( ( new CppUnit::TestCaller
context.addTest( ( new CppUnit::TestCaller
}
//注意,这里suite()是static类型方法
static CppUnit::TestSuite *suite()
{
const CppUnit::TestNamer &namer = getTestNamer__();
std::auto_ptr
CppUnit::ConcretTestFixtureFactory
CppUnit::TestSuiteBuilderContextBase context(*suite.get(), namer, factory);
TestFixtureType::addTestsToSuite( context );
return suite.release();
}
private:
typedef int CppUnitDummyTypedefForSemiColonEnding__;
protected:
double m_value1;
double m_value2;
public:
//setUp和tearDown是TestFixture的虚拟函数,子类可以不实现。
void setUp();
//void tearDown();
protected:
//以下是用户自己编写的测试方法,至于为什么返回类型都是void,而且无参数否则编译会不通过,在后面分析TestCaller的时候会讲到原因。
void example();
void anotherExample();
void testAdd();
void testEquals();
};
//在AutoRegisterSuite实例对象的构造函数中完成了TestSuite的登记操作
static CppUnit::AutoRegisterSuite< ExampleTestCase > autoRegisterRegistry__4;
void ExampleTestCase::example()
{
//下面这些方法,当断言条件不成立的时候则抛出Exception,Exception中有断言的表达
//式和代码位置信息,这个Exception由外层处理并记录到TestResult
(
CppUnit::assertDoubleEquals( (1.0), (1.1), (0.05),CppUnit::SourceLine( "ExampleTestCase.cpp", 8 ), "" ) );
( CppUnit::Asserter::failIf( !(1 == 0), CppUnit::Message( "assertion failed", "Expression: " "1 == 0"), CppUnit::SourceLine( "ExampleTestCase.cpp", 9 ) ) );
( CppUnit::Asserter::failIf( !(1 == 1), CppUnit::Message( "assertion failed", "Expression: " "1 == 1"), CppUnit::SourceLine( "ExampleTestCase.cpp", 10 ) ) );
}
void ExampleTestCase::anotherExample()
{
( CppUnit::Asserter::failIf( !(1 == 2), CppUnit::Message( "assertion failed", "Expression: " "1 == 2"), CppUnit::SourceLine( "ExampleTestCase.cpp", 16 ) ) );
}
void ExampleTestCase::setUp()
{
m_value1 = 2.0;
m_value2 = 3.0;
}
void ExampleTestCase::testAdd()
{
double result = m_value1 + m_value2;
( CppUnit::Asserter::failIf( !(result == 6.0), CppUnit::Message( "assertion failed", "Expression: " "result == 6.0"), CppUnit::SourceLine( "ExampleTestCase.cpp", 28 ) ) );
}
void ExampleTestCase::testEquals()
{
long* l1 = new long(12);
long* l2 = new long(12);
( CppUnit::assertEquals( (12), (12),CppUnit::SourceLine( "ExampleTestCase.cpp", 37 ), "" ) );
( CppUnit::assertEquals( (12L), (12L),CppUnit::SourceLine( "ExampleTestCase.cpp", 38 ), "" ) );
( CppUnit::assertEquals( (*l1), (*l2),CppUnit::SourceLine( "ExampleTestCase.cpp", 39 ), "" ) );
delete l1;
delete l2;
( CppUnit::Asserter::failIf( !(12L == 12L), CppUnit::Message( "assertion failed", "Expression: " "12L == 12L"), CppUnit::SourceLine( "ExampleTestCase.cpp", 44 ) ) );
( CppUnit::assertEquals( (12), (13),CppUnit::SourceLine( "ExampleTestCase.cpp", 45 ), "" ) );
( CppUnit::assertDoubleEquals( (12.0), (11.99), (0.5),CppUnit::SourceLine( "ExampleTestCase.cpp", 46 ), "" ) );
}
3. 源码解读
使用CPPUNIT进行单元测试的程序一般包含三个过程(从main函数可以看出来)
l 测试对象的构建
l 运行测试对象
l 测试结果信息收集与显示(在每一个代码片段后面都会给出函数调用序列)。
(具体参考源代码。源文排版混乱,不做转载)