CppUnit使用指南
测试驱动开发的原则:
Ø 先写测试代码,然后编写符合测试的代码。至少做到完成部分代码后,完成对应的测试代码;
Ø 测试代码不需要覆盖所有的细节,但应该对所有主要的功能和可能出错的地方有相应的测试用例;
Ø 发现 bug,首先编写对应的测试用例,然后进行调试;
Ø 不断总结出现 bug 的原因,对其他代码编写相应测试用例;
Ø 每次编写完成代码,运行所有以前的测试用例,验证对以前代码影响,把这种影响尽早消除;
Ø 不断维护测试代码,保证代码变动后通过所有测试;
Ø 在编码前:他可以强迫你对需求进行详细的分析。
Ø 在编码时:他可以使你对over coding保持警觉。
Ø 在重构时:可以确保新的设计能够兼容旧版本的功能。
Ø 在团队开发时:可以确保自己的单元是无误的。
CppUnit的原理
在 CppUnit 中,一个或一组测试用例的测试对象被称为 Fixture(设施,下文为方便理解尽量使用英文名称)。Fixture 就是被测试的目标,可能是一个对象或者一组相关的对象,甚至一个函数。
有了被测试的 fixture,就可以对这个 fixture 的某个功能、某个可能出错的流程编写测试代码,这样对某个方面完整的测试被称为TestCase(测试用例)。通常写一个 TestCase 的步骤包括:
1. 对 fixture 进行初始化,及其他初始化操作,比如:生成一组被测试的对象,初始化值;
2. 按照要测试的某个功能或者某个流程对 fixture 进行操作;
3. 验证结果是否正确;
4. 对 fixture 的及其他的资源释放等清理工作。
对 fixture 的多个测试用例,通常(1)(4)部分代码都是相似的,CppUnit 在很多地方引入了 setUp 和 tearDown 虚函数。可以在 setUp 函数里完成(1)初始化代码,而在 tearDown 函数中完成(4)代码。具体测试用例函数中只需要完成(2)(3)部分代码即可,运行时 CppUnit 会自动为每个测试用例函数运行 setUp,之后运行 tearDown,这样测试用例之间就没有交叉影响。
撰写TestCase必须注意以下几点:
Ø 可以自动执行,不用人手操作。
Ø 自动返回测试结果。
Ø 绝对的独立,不能与其他TestCase有任何联系。就算测试同一个函数的不同功能也需要分开。每个TestCase可以说是一个孤岛。
对 fixture 的所有测试用例可以被封装在一个 CppUnit::TestFixture 的子类(命名惯例是[ClassName]Test)中。然后定义这个fixture 的 setUp 和 tearDown 函数,为每个测试用例定义一个测试函数(命名惯例是 testXXX)。下面是个简单的例子:
class MathTest : public CppUnit::TestFixture {
protected:
int m_value1, m_value2;
public:
MathTest() {}
// 初始化函数
void setUp () {
m_value1 = 2;
m_value2 = 3;
}
// 测试加法的测试函数
void testAdd () {
// 步骤(2),对 fixture 进行操作
int result = m_value1 + m_value2;
// 步骤(3),验证结果是否争取
CPPUNIT_ASSERT( result == 5 );
}
// 没有什么清理工作没有定义 tearDown.
}
在测试函数中对执行结果的验证成功或者失败直接反应这个测试用例的成功和失败。CppUnit 提供了多种验证成功失败的方式:
CPPUNIT_ASSERT(condition) // 确信condition为真
CPPUNIT_ASSERT_MESSAGE(message, condition) // 当condition为假时失败, 并打印message
CPPUNIT_FAIL(message) // 当前测试失败, 并打印message
CPPUNIT_ASSERT_EQUAL(expected, actual) // 确信两者相等
CPPUNIT_ASSERT_EQUAL_MESSAGE(message, expected, actual) // 失败的同时打印message
CPPUNIT_ASSERT_DOUBLES_EQUAL(expected, actual, delta) // 当expected和actual之间差大于delta时失败
要把对 fixture 的一个测试函数转变成一个测试用例,需要生成一个 CppUnit::TestCaller 对象。而最终运行整个应用程序的测试代码的时候,可能需要同时运行对一个 fixture 的多个测试函数,甚至多个 fixture 的测试用例。CppUnit 中把这种同时运行的测试案例的集合称为 TestSuite。而 TestRunner 则运行测试用例或者 TestSuite,具体管理所有测试用例的生命周期。目前提供了 3 类TestRunner,包括:
Ø CppUnit::TextUi::TestRunner // 文本方式的TestRunner
Ø CppUnit::QtUi::TestRunner // QT方式的TestRunner
Ø CppUnit::MfcUi::TestRunner // MFC方式的TestRunner
下面是一个TestRunner的例子:
CppUnit::TextUi::TestRunner runner;
CppUnit::TestSuite *suite= new CppUnit::TestSuite();
// 添加一个测试用例
suite->addTest(new CppUnit::TestCaller<MathTest> (
"testAdd", testAdd));
// 指定运行TestSuite
runner.addTest( suite );
// 开始运行, 自动显示测试进度和测试结果
runner.run( "", true ); // Run all tests and wait
常用使用方式
按照上面的方式,如果要添加新的测试用例,需要把每个测试用例添加到 TestSuite 中,而且添加新的 TestFixture 需要把所有头文件添加到 main.cpp 中,比较麻烦。为此 CppUnit 提供了 CppUnit::TestSuiteBuilder,CppUnit::TestFactoryRegistry 和一堆宏,用来方便地把 TestFixture 和测试用例注册到 TestSuite 中。下面就是通常的使用方式(注意红色字体):
/// MathTest.h
#include "cppunit/extensions/HelperMacros.h"
class MathTest : public CppUnit::TestFixture {
// 声明一个TestSuite
CPPUNIT_TEST_SUITE( MathTest );
// 添加测试用例到TestSuite, 定义新的测试用例需要在这儿声明一下
CPPUNIT_TEST( testAdd );
// TestSuite声明完成
CPPUNIT_TEST_SUITE_END();
// 其余不变
protected:
int m_value1, m_value2;
public:
MathTest() {}
// 初始化函数
void setUp ();
// 清理函数
void tearDown();
// 测试加法的测试函数
void testAdd ();
// 可以添加新的测试函数
};
/// MathTest.cpp
// 把这个TestSuite注册到名字为"alltest"的TestSuite中, 如果没有定义会自动定义
// 也可以CPPUNIT_TEST_SUITE_REGISTRATION( MathTest );注册到全局的一个未命名的TestSuite中.
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION( MathTest, "alltest" );
// 下面不变
void MathTest::setUp()
{
m_value1 = 2;
m_value2 = 3;
}
void MathTest::tearDown(){
}
void MathTest::testAdd(){
int result = m_value1 + m_value2;
CPPUNIT_ASSERT( result == 5 );
}
/// main.cpp
// 不用再包含所有TestFixture子类的头文件
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TestRunner.h>
// 如果不更改TestSuite, 本文件后期不需要更改.
int main()
{
CppUnit::TextUi::TestRunner runner;
// 从注册的TestSuite中获取特定的TestSuite, 没有参数获取未命名的TestSuite.
CppUnit::TestFactoryRegistry ®istry = CppUnit::TestFactoryRegistry::getRegistry("alltest");
//若没有注册名字,在MathTest.cpp文件中只是调用了CPPUNIT_TEST_SUITE_REGISTRATION( MathTest );那就可以写成
CppUnit::TestFactoryRegistry ®istry = CppUnit::TestFactoryRegistry::getRegistry();
// 添加这个TestSuite到TestRunner中
runner.addTest( registry.makeTest() );
// 运行测试
runner.run();
}
/// mainanother.cpp
另外的一种写法。其实和上面大同小异,只是生成的对象是Test而不是registry了。
int main(int argc, char* argv[])
{
// Get the top level suite from the registry
CppUnit::Test *suite = CppUnit::TestFactoryRegistry::getRegistry().makeTest();
// Adds the test to the list of test to run
CppUnit::TextUi::TestRunner runner;
runner.addTest( suite );
// Change the default outputter to a compiler error format outputter
runner.setOutputter( new CppUnit::CompilerOutputter( &runner.result(),
std::cerr ) );
// Run the tests.
bool wasSucessful = runner.run();
// Return error code 1 if the one of test failed.
return wasSucessful ? 0 : 1;
}
这样添加新的测试用例(TestFixture)只需要在类定义的开始声明一下即可。
其他实际问题
通常包含测试用例代码和被测试对象是在不同的项目中。应该在另一个项目(最好在不同的目录)中编写 TestFixture,然后把被测试的对象包含在测试项目中。
对某个类或者某个函数进行测试的时候,这个 TestFixture 可能引用了别的类或者别的函数,为了隔离其他部分代码的影响,应该在源文件中临时定义一些桩程序,模拟这些类或者函数。这些代码可以通过宏定义在测试项目中有效,而在被测试的项目中无效。
CppUnit的组成结构
2 核心部分(Core)
2.1 基本测试类
2.1.1 Test
2.1.2 TestFixture
2.1.3 TestCase
2.1.4 TestSuite
2.2 测试结果记录
2.2.1 SynchronizedObject
2.2.2 TestListener
2.2.3 TestResult
2.3 错误处理
2.3.1 TestFailure
2.3.2 SourceLine
2.3.3 Exception
2.3.4 NotEqualException
2.4 断言
2.4.1 A sserter
2.4.2 TestAssert
3 输出部分(Output)
3.1 基础部件
3.1.1 Outputter
3.1.2 TestResultCollector
3.2 衍生类
3.2.1 TextOutputter
3.2.2 CompilerOutputter
3.2.3 XmlOutputter
4 辅助部分(Helper)
4.1 创建机制
4.1.1 TypeInfoHelper
4.1.2 TestFactory
4.1.3 TestFactoryRegistry,NamedRegistries
4.1.4 TestSuiteFactory
4.1.5 TestSuiteBuilder
4.1.6 TestCaller
4.1.7 A utoRegisterSuite
4.2 HelperMacros
5 扩展部分(Extension)
5.1 TestDecorator
5.2 RepeatedTest
5.3 Orthodox
5.4 TestSetUp
6 兼听者部分(Listener)
6.1 TestSucessListener
6.2 TextTestProgressListener
6.3 TextTestResult
7 界面部分(TextUI)
7.1 TestRunner
8 移植(Portability)
8.1 OStringStream
8.2 其他在CppUnit中,有一个贯穿始终的最基本的pattern,那便是Composite Pattern。在GoF中对该pattern有如下描述:将对象组合成树形结构以表示“部分-整体”的层次结构。Composite使得用户对单个对象 和组合对象的使用具有一致性。在CppUnit的框架中,测试类分为两种,某些测试类代表单个测试,比如稍后讲到的TestCase(测试用例),另一些 则由若干测试类共同构成,比如稍后讲到的TestSuite(测试包)。彼此相关的TestCase共同构成一个TestSuite,而 TestSuite也可以嵌套包含。两者分别对应Composite Pattern中的Leaf和Composite。
这是所有测试类的抽象基类,规定了所有测试类都应该具有的行为,对应于Composite Pattern中的Component,除了标准的virtual dtor外,还定义了四个纯虚函数:
// 运行测试内容,并利用传入其内的TestResult搜集测试结果,类似Component的Operation操作 virtual void run (TestResult *result) = 0; // 返回当前包含的测试对象的个数,若为TestCase,则返回1。 virtual int countTestCases () const = 0; // 返回测试的名称,每个测试都有一个名称,就像是标识,用以查找或显示 virtual std::string getName () const = 0; // 本测试的简短描述,用于调试输出。 // 对测试的描述除了名称外,可能还有其他信息, // 比如:一个名为“complex_add”的测试包可能被描述成“suite complex_add” virtual std::string toString () const = 0;
该类也是抽象类,用于包装测试类使之具有setUp方法和tearDown方法。利用它,可以为一组相关的测试提供运行所需的公用环境(即所谓的fixture)。要实现这一目的,你需要:
- 从TestFixture派生一个子类(事实上,一般的做法是从TestCase派生,这样比较方便,具体见后)
- 定义实例变量(instance variables)以形成fixture
- 重载setUp初始化fixture的状态
- 重载tearDown在测试结束后作资源回收工作
此外,作为完整的测试类,还要定义一些执行具体测试任务的测试方法,然后使用TestCaller进行测试。关于TestCaller,在helper部分将会讲到。
因为每个测试对象运行在其自身的fixture中,所以测试对象之间不会有副作用(side effects),而测试对象内部的测试方法则共同使用同一个fixture。
来看一下TestFixture的定义,除了标准的virtual dtor外,还定义了两个纯虚函数:
// 在运行测试之前设置其上下文,即fixture// 一般而言setUp更为重要些,除非实例变量创建于heap中,否则其资源的回收就无需手工处理了virtual void setUp() {};// 在测试运行结束之后进行资源回收virtual void tearDown() {};
TestCase中最重要的方法是run方法,来看一下代码,并请留意morning的注释:
void TestCase::run( TestResult *result ){ // 不必关心startTest的具体行为,在讲到TestResult时自然会明白 // 末尾的endTest亦是如此 result->startTest(this); try { // 设置fixture,具体内容需留待派生类解决 // 可能有异常抛出,处理方式见后 setUp(); // runTest具有protected属性,是真正执行测试的函数 // 但具体行为需留待派生类解决 try { runTest(); } // 在运行测试时可能会抛出异常,以下是异常处理 catch ( Exception &e ) { // Prototype Pattern的一个应用 // e是临时对象,addFailure调用之后即被销毁,所以需要创建一个副本 Exception *copy = e.clone(); result->addFailure( this, copy ); } catch ( std::exception &e ) { // 异常处理的常用方法——转意 result->addError( this, new Exception( e.what() ) ); } catch (...) { // 截获其余未知异常,一网打尽 Exception *e = new Exception( "caught unknown exception" ); result->addError( this, e ); } // 资源回收 try { tearDown(); } catch (...) { result->addError( this, new Exception( "tearDown() failed" ) ); } } catch (...) { result->addError( this, new Exception( "setUp() failed" ) ); } result->endTest( this );}
可以看到,run方法定义了一个测试类运行的基本行为及其顺序:
TestCase( std::string Name ); // 测试类的名称TestCase();
此外,TestCase将copy ctor和operator=声明为private属性,以防止误用。
[TestSuite]
相关文件:TestSuite.h,TestSuite.cpp
一 组相互关联的测试用例,构成了一个测试包,这就是TestSuite,也就是Composite Pattern中的Composite。和TestCase一样,也派生自Test,只是没有fixture特性。除了测试类的名称外,在 TestSuite中还维护了一个测试对象数组,它被声明为private属性:
std::vector<Test *> m_tests; // [可否使用vector<Test &>呢?]const std::string m_name;
来看一下TestSuite的run方法是如何实现的,并请留意morning的注释:
void TestSuite::run( TestResult *result ){ // 遍历vector<Test *> for ( std::vector<Test *>::iterator it = m_tests.begin(); it != m_tests.end(); ++it ) { // 可能中途终止 if ( result->shouldStop() ) break; Test *test = *it; // 调用每个test自己的run // 可能是TestCase实例,也可能是TestSuite实例, // 后者形成递归,但此处却全然不知 test->run( result ); }}
关 于TestResult及其shouldStop方法,稍后会讲到。不过此处的break,到也算是活用Composite Pattern的一个简单范例。从效率的角度考虑,当确信不必再执行后续的test时,即可直接返回,而不是照葫芦画瓢,简单的调用一下test的run 方法。
既然TestResult派生自Test,那么countTestCases又是如何实现的呢:
int TestSuite::countTestCases() const{ int count = 0; // 遍历vector<Test *> for ( std::vector<Test *>::const_iterator it = m_tests.begin(); it != m_tests.end(); ++it ) count += (*it)->countTestCases(); // 递归调用每个test的countTestCases,并累加 return count;}
至于addTest,自然是不能少的,它对应于Composite的Add方法:
void TestSuite::addTest( Test *test ){ m_tests.push_back( test ); // 将test添加到测试对象数组的尾端}
不过请注意,addTest方法并未出现于抽象类Test中,关于这类设计上的权衡在GoF中,Composite Pattern一节有专门的论述。
TestSuite管理着其下所属诸测试对象的生命周期,在dtor中,它会调用deleteContents方法:
void TestSuite::deleteContents(){ for ( std::vector<Test *>::iterator it = m_tests.begin(); it != m_tests.end(); ++it) delete *it; m_tests.clear();}
此外,TestSuite还为外部访问其所属测试对象提供了接口,因为返回值是const &类型的,所以是read-only的:
const std::vector<Test *> &getTests() const;