CppUnit源码解读

本教程版权为晨光(Morning)所有,未经允许,请勿复制、传播,谢谢。(http://morningspace.51.net/)

序言


[引言] [CppUnit的简单身世] [CppUnit的总体构成] [几点说明]

核心部分(Core)——基本测试类


在CppUnit中,有一个贯穿始终的最基本的pattern,那便是Composite Pattern。在GoF中对该pattern有如下描述:将对象组合成树形结构以表示“部分-整体”的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。在CppUnit的框架中,测试类分为两种,某些测试类代表单个测试,比如稍后讲到的TestCase(测试用例),另一些则由若干测试类共同构成,比如稍后讲到的TestSuite(测试包)。彼此相关的TestCase共同构成一个TestSuite,而TestSuite也可以嵌套包含。两者分别对应Composite Pattern中的Leaf和Composite。

[Test] [TestFixture] [TestCase] [TestSuite] [Outputter] [TestResultCollector]

相关文件:Test.h

这是所有测试类的抽象基类,规定了所有测试类都应该具有的行为,对应于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;

 

相关文件:TestFixture.h

该类也是抽象类,用于包装测试类使之具有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.h,TestCase.cpp

派生自Test和TestFixture(多重继承),兼具两者特性,用于实现一个简单的测试用例。你所要做的就是派生该类,并重载runTest方法。不过通常你不必如此,而是使用TestCaller结合TestFixture的方法,这样很方便。当你发现TestCaller无法满足,你需要重写一个功能近似的类时,再使用TestCase也不迟。关于TestCaller,在helper部分将会讲到。

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方法定义了一个测试类运行的基本行为及其顺序:

  • setUp:准备
  • runTest:开始
  • tearDown:结束

而TestCase作为抽象类无法确定测试的具体行为,因此需要留待派生类解决,这就是Template Method Pattern。事实上,该pattern在framework中是很常见的。因此一个完整测试的简单执行方法是,从TestCase派生一个类,重载相关方法,并直接调用run方法(正如TestFixture中所提到的)。

有意思的是,TestCase中还有run的另一个版本,它没有形参,而是创建一个缺省的TestResult,然后调用前述run方法。不过好像没怎么用到,大概是先前调试时未及清理的垃圾代码,也难怪会有“FIXME: what is this for?”这样的注释了。

TestCase有两个ctor:

TestCase( std::string Name );        // 测试类的名称
TestCase();

后者主要用于TestCaller,因为在使用TestCaller时,需要一个default ctor

此外,TestCase将copy ctor和operator=声明为private属性,以防止误用。

 

相关文件:TestSuite.h,TestSuite.cpp

一组相互关联的测试用例,构成了一个测试包,这就是TestSuite,也就是Composite Pattern中的Composite。和TestCase一样,也派生自Test,只是没有fixture特性。除了测试类的名称外,在TestSuite中还维护了一个测试对象数组,它被声明为private属性:

std::vector<Test *> m_tests;
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;

输出部分(Output)——基础部件


这一部分主要提供了一些用于输出测试结果的工具类,输出的方式可以有多种,比如:以纯文本方式输出,以XML标记语言方式输出,基于IDE开发环境的输出等。由此足见,CppUnit的实现者想得还是很周到的。

 

相关文件:Outputter.h

这是一系列测试结果输出类的抽象基类,只有寥寥几行代码,唯一的作用是定义了一个write操作和一个virtual dtor:

virtual ~Outputter() {}
virtual void write() =0;

由于各种输出方式,其具体实现大相径庭,所以Outputter所能做的也止于此了。

 

相关文件:TestResultCollector.h,TestResultCollector.cpp

该类派生自TestSucessListener,同样也是TestListener和SynchronizedObject,因为前者派生自后二者。关于TestSucessListener请见listener部分,关于TestListener请见core部分。TestResultCollector的作用是搜集正在执行的测试用例的结果。依源码中的documentation comments所述,这是Collecting Parameter Pattern的一个应用[该Pattern在GoF中没有提及,morning有些孤陋寡闻]

TestSucessListene定义了三个成员变量,用来记录测试相关信息:

std::deque<Test *> m_tests;            // 指针队列用以记录测试对象
std::deque<TestFailure *> m_failures;  // 指针队列用以记录测试失败信息
int m_testErrors;                      // 用以记录测试错误个数

TestSucessListene还覆盖了基类TestListener的startTest,reset和addFailure方法:

void TestResultCollector::startTest( Test *test )
{
  ExclusiveZone zone (m_syncObject); 
  m_tests.push_back( test );               // 将测试对象加入链表中
}

void TestResultCollector::reset()
{
  TestSucessListener::reset();

  ExclusiveZone zone( m_syncObject ); 
  m_testErrors = 0;
  m_tests.clear();
  m_failures.clear();
}

void TestResultCollector::addFailure( const TestFailure &failure )
{
  TestSucessListener::addFailure( failure );

  ExclusiveZone zone( m_syncObject ); 
  if ( failure.isError() )                   // 若failure实为error,则m_testErrors加1
    ++m_testErrors;
  m_failures.push_back( failure.clone() );   // 此处用了clone
}

这里使用了ExclusiveZone,关于failure和error的差别,以及clone方法,请见core部分。由于使用了clone方法,所以在addFailure中创建的failure需要在dtor中回收:

TestResultCollector::~TestResultCollector()
{
  TestFailures::iterator itFailure = m_failures.begin();
  while ( itFailure != m_failures.end() )
    delete *itFailure++;
}

此外,就是几个getter方法了:

// 获取运行的测试个数
int TestResultCollector::runTests() const
{ 
  ExclusiveZone zone( m_syncObject ); 
  return m_tests.size(); 
}

// 获取运行错误的测试个数
int TestResultCollector::testErrors() const
{ 
  ExclusiveZone zone( m_syncObject );
  return m_testErrors;
}

// 获取运行失败的测试个数
int TestResultCollector::testFailures() const
{ 
  ExclusiveZone zone( m_syncObject ); 
  return m_failures.size() - m_testErrors;
}

// 获取运行错误及失败的测试的总个数
int TestResultCollector::testFailuresTotal() const
{
  ExclusiveZone zone( m_syncObject ); 
  return m_failures.size();
}

// 获取记录测试失败的链表
const TestResultCollector::TestFailures & 
TestResultCollector::failures() const
{ 
  ExclusiveZone zone( m_syncObject );
  return m_failures; 
}

// 获取记录测试对象的链表
const TestResultCollector::Tests &
TestResultCollector::tests() const
{
  ExclusiveZone zone( m_syncObject );
  return m_tests;
}

这里还想提一下有关TestResult和TestResultCollector的区别。根据随CppUnit所附的ChangeLog中的“记载”,早先版本的CppUnit中只有TestResult,它所实现的功能和TestResultCollector完全一样,像failures、testFailuresTotal等方法,原来都是在TestResult中的。不过在随后的refactoring过程中,由于引入了Observer Pattern,TestResultCollector应运而生,TestResult成了Subject,其原有记录测试运行结果的责任被移交给了TestResultCollector,而后者则是一个地道的Listener。于是原来在TestResult中出现的那些方法在Extract Method的“协助”之下被转移到了TestResultCollector中。

辅助部分(Helper)——创建机制


这一部分提供了一些辅助类,多数与创建Test类的实例有关,其中包括用于创建Test的工厂类,用于管理工厂类的注册类,可以单独运行某个测试的TestCaller,还有为方便使用而定义的一组宏。

[TypeInfoHelper] [TestFactory] [TestFactoryRegistry,NamedRegistries] [TestSuiteFactory] [TestSuiteBuilder] [TestCaller] [AutoRegisterSuite]  

相关文件:TypeInfoHelper.h,TypeInfoHelper.cpp

为了扫清理解障碍,TypeInfoHelper是首先需要解释的。该类的作用是根据指定类的type_info返回一个代表其类名的字符串。为了使用此功能,你必须定义CPPUNIT_USE_TYPEINFO_NAME宏,即你必须确认你所使用的c++编译器提供了type_info机制。TypeInfoHelper仅有一个static成员函数getClassName,请留意morning的注释:

std::string 
TypeInfoHelper::getClassName( const std::type_info &info )
{
  static std::string classPrefix( "class " );
  std::string name( info.name() );      // 调用info的name以得到类名信息

  // 确定类名中是否有"class"字样
  bool has_class_prefix = 0 ==
#if CPPUNIT_FUNC_STRING_COMPARE_STRING_FIRST
    name.compare( classPrefix, 0, classPrefix.length() );
#else
    name.compare( 0, classPrefix.length(), classPrefix );
#endif

  // 返回不带有"class"字样的类名
  return has_class_prefix ? name.substr( classPrefix.length() ) : name;
}

关于此处用到的std::string::compare函数,在bcb和vc中的调用方式不一样,所以就有了CPPUNIT_FUNC_STRING_COMPARE_STRING_FIRST宏。参见config-msvc6.h和config-bcb5.h中的相关定义以及portability部分的说明。

 

相关文件:TestFactory.h

是Test的抽象类工厂(Abstract Factory),用于创建一个Test实例,它仅仅包含了一个纯虚函数makeTest的声明:

virtual Test* makeTest() = 0;

 

相关文件:TestFactoryRegistry.cpp,TestFactoryRegistry.cpp

某次测试的运行可能包含了许多测试实例,它们彼此间可能呈现层状结构,而每个测试实例的创建都是由某个与之对应的类工厂完成的。为了较好的管理这些类工厂,实现其生命周期的自动操控,CppUnit采用了一种注册机制。类TestFactoryRegistry和类NamedRegistries就是用来实现该机制的。

NamedRegistries是一个管理类,用以管理所有的注册项——TestFactoryRegistry类的实例,由它全权负责TestFactoryRegistry的生命周期。TestFactoryRegistry在稍后会讲到。

NamedRegistries采用了Singleton Pattern,以保证其“全局性”的唯一访问点。此处是通过在函数内定义静态变量的方式来实现的:

NamedRegistries &
NamedRegistries::getInstance()
{
  static NamedRegistries namedRegistries;
  return namedRegistries;
}

NamedRegistries内部有三个private属性的成员变量:

Registries m_registries;        // 代表一个注册名称-注册项的映射表
Factories m_factoriesToDestroy; // 代表即将被销毁的注册项序列
Factories m_destroyedFactories; // 代表已经被销毁的注册项序列

其中,Registries和Factories的定义如下:

typedef std::map<std::string, TestFactoryRegistry *> Registries;
typedef std::set<TestFactory *> Factories;

为了使外界可以访问到注册项,NamedRegistries提供了getRegistry方法,请留意morning的注释:

TestFactoryRegistry &
NamedRegistries::getRegistry( std::string name )
{
  // 根据name在m_registries中查找注册项
  Registries::const_iterator foundIt = m_registries.find( name );
  // 若没有找到,则创建一个TestFactoryRegistry实例,并赋以name作为名称
  // 将之分别插入m_registries和m_factoriesToDestroy中
  // 再返回该TestFactoryRegistry实例
  if ( foundIt == m_registries.end() )
  {
    TestFactoryRegistry *factory = new TestFactoryRegistry( name );
    m_registries.insert( std::make_pair( name, factory ) );
    m_factoriesToDestroy.insert( factory );
    return *factory;
  }
  // 若找到,则直接返回
  return *foundIt->second;
}

在NamedRegistries被销毁(即dtor被调用)的同时,其下所属的TestFactoryRegistry实例也将被销毁:

NamedRegistries::~NamedRegistries()
{
  Registries::iterator it = m_registries.begin();
  while ( it != m_registries.end() )
  {
    TestFactoryRegistry *registry = (it++)->second;
    if ( needDestroy( registry ) )
      delete registry;
  }
}

这里加上needDestroy的判断是为了防止出现多次销毁同一个TestFactoryRegistry实例的现象,稍后可以发现这和TestFactoryRegistry的dtor实现有关,另外一个wasDestroyed方法,也与此有关,它们的实现代码分别如下:

void 
NamedRegistries::wasDestroyed( TestFactory *factory )
{
  // 从m_factoriesToDestroy中摘除factory
  m_factoriesToDestroy.erase( factory );
  // 将factory插入m_destroyedFactories
  m_destroyedFactories.insert( factory );
}

bool 
NamedRegistries::needDestroy( TestFactory *factory )
{
  // 判断m_destroyedFactories是否存在factory
  return m_destroyedFactories.count( factory ) == 0;
}

根据约定,TestFactory的注册项必须调用wasDestroyed方法,以表明一个TestFactoryRegistry实例已经被成功销毁了。同时,它也需要调用needDestroy以确信一个给定的TestFactory可以被允许销毁,即事先没有被其他TestFactoryRegistry实例销毁。

我们再来看看TestFactoryRegistry。其ctor只是简单的将传入其中的字符串赋给成员变量m_name,它代表了注册项的名称:

TestFactoryRegistry::TestFactoryRegistry( std::string name ) :
    m_name( name )
{
}

dtor稍微复杂一些,请留意morning的注释:

TestFactoryRegistry::~TestFactoryRegistry()
{
  // 还记得前面提到的约定吗?
  NamedRegistries::getInstance().wasDestroyed( this );

  // 遍历其下所属的各个TestFactory实例
  for ( Factories::iterator it = m_factories.begin(); it != m_factories.end(); ++it )
  {
    TestFactory *factory = it->second;
    // 若factory没有存在于NamedRegistries::m_destroyedFactories中
    // 则可以放心销毁之。factory的销毁可能形成连锁反应,亦即,若factory本身
    // 也是TestFactoryRegistry类型的,其dtor又将被调用,上述过程将再次重现
    if ( NamedRegistries::getInstance().needDestroy( factory ) )
      delete factory;
  }
}

这里我们再次看到了wasDestroyed和needDestroy,正如前面所述,它们是为了防止多次销毁同一个TestFactoryRegistry实例的。对于如下的代码:

  registerFactory( "All Tests", getRegistry( "Unit Tests" ) );

在了解了TestFactoryRegistry::getRegistry的实际行为之后,你会发现,名为“Unit Tests”的注册项将同时被名为"All Tests"的注册项和NamedRegistries所拥有。此外,morning以为,对于没有在NamedRegistries中注册的TestFactoryRegistry实例,调用needDestroy的结果同样为true,此处也保证可以被及时销毁,而不致造成内存泄漏。由此足见,此处的注册机制是相当灵活的。

上面出现的m_factories是TestFactoryRegistry的一个private属性的成员变量:

Factories m_factories;  // 代表一个类工厂名称-类工厂实例的映射表

Factories的定义如下:

typedef std::map<std::string, TestFactory *> Factories;

至于getRegistry方法,则有两个版本,一个有形参,另一个则没有:

TestFactoryRegistry &
TestFactoryRegistry::getRegistry()
{
  // 调用另一个版本的getRegistry,并传入“All Tests”
  // 一般代表某组测试的“根节点”
  return getRegistry( "All Tests" );
}

TestFactoryRegistry &
TestFactoryRegistry::getRegistry( const std::string &name )
{
  // 获取NamedRegistries的实例,并调用其getRegistry方法
  return NamedRegistries::getInstance().getRegistry( name );
}

前面提到,某个类工厂的注册项,其下可能包含一组类工厂,即形成所谓的层状结构。为了支持这一功能,TestFactoryRegistry提供了registerFactory方法,同样有两个不同版本:

// 给定类工厂的名称及其对应的实例指针
void 
TestFactoryRegistry::registerFactory( const std::string &name,
                                      TestFactory *factory )
{
  m_factories[name] = factory;
}

// 只给出了类工厂的实例指针,此之渭Unnamed TestFactory
void 
TestFactoryRegistry::registerFactory( TestFactory *factory )
{
  // 通过serialNumber自动形成名称,再调用另一个版本的registerFactory
  // static变量serialNumber从1开始,每次累加1
  static int serialNumber = 1;

  OStringStream ost;
  ost << "@Dummy@" << serialNumber++;

  registerFactory( ost.str(), factory );
}

当层状结构构建好后,就可以调用makeTest方法,创建待运行的测试实例了,请留意morning的注释:

Test *
TestFactoryRegistry::makeTest()
{
  // 创建一个测试包,并冠以m_name的名称
  TestSuite *suite = new TestSuite( m_name );
  // 调用addTestToSuite,以将其下所属的测试实例添加到suite中
  addTestToSuite( suite );
  // 返回测试包对应的指针
  return suite;
}

void 
TestFactoryRegistry::addTestToSuite( TestSuite *suite )
{
  // 遍历其下所属的各个TestFactory实例
  for ( Factories::iterator it = m_factories.begin(); 
        it != m_factories.end(); 
        ++it )
  {
    TestFactory *factory = (*it).second;
    // 调用factory的makeTest方法创建测试实例
    // 将指向实例的指针添加到suite中
    // makeTest的调用可能形成连锁反应
    suite->addTest( factory->makeTest() );
  }
  // 当所有的factory都遍历完后,即所有的测试实例都被创建成功后
  // 整个测试实例的层状结构也就构建成功了
}

以下对CppUnit的注册机制作一个简单的小结:

  • NamedRegistries以“线性”方式管理着所有的类工厂注册项——TestFactoryRegistry,负责维护其生命周期
  • 一个TestFactoryRegistry对应一个类工厂实例,其下可能包含一系列的类工厂实例,构成层状结构
  • TestFactory(s)可以注册到NamedRegistries中,也可以不注册,它们要么被NamedRegistries销毁,要么被其所属之TestFactoryRegistry销毁
  • 所有的类工厂都注册完毕后,TestFactoryRegistry::makeTest方法的一次调用,将形成连锁反映(即递归调用),以创建一组具有层状结构的测试实例

以下提供几个使用TestFactoryRegistry类的例子,以加深认识:

// 例1:创建一个空的测试包,并将与之对应的类工厂注册项注册到NamedRegistries中
CppUnit::TestFactoryRegistry ®istry = CppUnit::TestFactoryRegistry::getRegistry();
CppUnit::TestSuite *suite = registry.makeTest();

// 例2:创建一个名为“Math”的测试包,并将与之对应的类工厂注册项注册到NamedRegistries中
CppUnit::TestFactoryRegistry &mathRegistry = CppUnit::TestFactoryRegistry::getRegistry( "Math" );
CppUnit::TestSuite *mathSuite = mathRegistry.makeTest();

// 例3:创建一个名为“All tests”的测试包,并将名为“Graph”和“Math”的测试包作为“All tests”测试包的子项
// 与全部三个测试包对应的类工厂注册项都被注册到NamedRegistries中
CppUnit::TestSuite *rootSuite = new CppUnit::TestSuite( "All tests" );
rootSuite->addTest( CppUnit::TestFactoryRegistry::getRegistry( "Graph" ).makeTest() );
rootSuite->addTest( CppUnit::TestFactoryRegistry::getRegistry( "Math" ).makeTest() );
CppUnit::TestFactoryRegistry::getRegistry().addTestToSuite( rootSuite );

// 例4:例3的另一中实现方式
CppUnit::TestFactoryRegistry ®istry = CppUnit::TestFactoryRegistry::getRegistry();
registry.registerFactory( CppUnit::TestFactoryRegistry::getRegistry( "Graph" ) );
registry.registerFactory( CppUnit::TestFactoryRegistry::getRegistry( "Math" ) );
CppUnit::TestSuite *suite = registry.makeTest();

 

相关文件:TestSuiteFactory.h

模板类,派生自TestFactory,是TestFixture的类工厂,并且该TestFixture必须实现一个静态的suite方法,以便在覆盖TestFactory的makeTest时调用:

// 此处的TestCaseType就是一个TestFixture
template<typename TestCaseType>
class TestSuiteFactory : public TestFactory
{
public:
  virtual Test *makeTest()
  {
    return TestCaseType::suite();
  }
};

 

相关文件:TestSuiteBuilder.h

模板类,用以将一系列测试添加到一个测试包中。所有加入该测试包的测试,其固有名称之前都会被加上测试包的名称,形成类似如下的测试名称:MyTestSuiteName.myTestName,前者为测试包的名称,后者为测试本身的名称。

TestSuiteBuilder内部维护了一个m_suite指针以指向对应的测试包实例,这是一个Smart Pointer,因此其生命周期无需手工操控,而是由TestSuiteBuilder来维护:

std::auto_ptr<TestSuite> m_suite;

至于m_suite所指的对象,可以由TestSuiteBuilder自己创建,也可以从外面传入,全凭你选择调用ctor的哪个版本了:

// 使用type_info生成测试包的名称
#if CPPUNIT_USE_TYPEINFO_NAME
TestSuiteBuilder() : 
    m_suite( new TestSuite( 
        TypeInfoHelper::getClassName( typeid(Fixture) )  ) )
{
}
#endif

TestSuiteBuilder( TestSuite *suite ) : m_suite( suite ) 
{
}

TestSuiteBuilder(std::string name) : m_suite( new TestSuite(name) ) 
{
}

添加测试的方法是简单地调用m_suite的addTest:

void addTest( Test *test )
{
  m_suite->addTest( test );
}

此外,为了方便使用,TestSuiteBuilder还提供了几个用于添加TestCaller的方法,它们调用makeTestName以生成测试名称,最终都将调用addTest。其中,Fixture是TestSuiteBuilder的模板类型参数,TestMethod的定义如下:

typedef void (Fixture::*TestMethod)();

至于TestCaller,稍后会讲到:

void addTestCaller(std::string methodName, 
                   TestMethod testMethod )
{
  Test *test = 
      new TestCaller<Fixture>( makeTestName( methodName ), 
                               testMethod );
  addTest( test );
}

void addTestCaller(std::string methodName, 
                   TestMethod testMethod, 
                   Fixture *fixture )
{
  Test *test = 
      new TestCaller<Fixture>( makeTestName( methodName ), 
                               testMethod,
                               fixture);
  addTest( test );
}

template<typename ExceptionType>
void addTestCallerForException(std::string methodName, 
                               TestMethod testMethod, 
                               Fixture *fixture,
                               ExceptionType *dummyPointer )
                               // dummyPointer本身没有实际作用,此处只为获取其所属类型
{
  Test *test = new TestCaller<Fixture,ExceptionType>( 
                               makeTestName( methodName ), 
                               testMethod,
                               fixture);
  addTest( test );
}

makeTestName的定义如下:

std::string makeTestName( const std::string &methodName )
{
  return m_suite->getName() + "." + methodName;
}

为了便于外界访问m_suite指针,TestSuiteBuilder还提供了如下辅助方法:

TestSuite *suite() const
{
  return m_suite.get();
}

TestSuite *takeSuite()
{
  return m_suite.release();
}

 

相关文件:TestCaller.h

在前面以及core部分曾经多次提到TestCaller,此类的作用是根据一个fixture创建一个测试用例。当你需要单独运行某个测试,或者要将其添加到某个测试包中时,你就可以使用TestCaller。一个TestCaller仅对应一个Test类,该Test类和一个TestFixture相关联。下面是一个演示的例子:

// 一个TestFixture,并包含了test method(s)
class MathTest : public CppUnit::TestFixture {
    ...
  public:
    void         setUp();
    void         tearDown();

    void         testAdd();
    void         testSubtract();
};

CppUnit::Test *MathTest::suite() {
  CppUnit::TestSuite *suite = new CppUnit::TestSuite;

  // 将MathTest::testAdd加入TestCaller,并将该TestCaller加入测试包中
  suite->addTest( new CppUnit::TestCaller<MathTest>( "testAdd", testAdd ) );
  return suite;
}

你可是使用TestCaller,将任意一个test方法和某个TestFixture绑定在一起,只要该test方法满足如下形式的定义:

void testMethod(void);

TestCaller其实是一个模板类,它派生自TestCase,有两个模板类型参数,前一个参数代表了TestFixture类,后一个参数代表某个异常类,缺省类型为NoExceptionExpected,至于该参数的作用,稍后便知分晓。关于TestCase,请见core部分:

template <typename Fixture, 
	  typename ExpectedException = NoExceptionExpected>
class TestCaller : public TestCase
{
  //...
};

TestCaller有三个private属性的成员变量:

Fixture *m_fixture;     // 指向TestFixture实例的指针
bool m_ownFixture;      // 若为true,则由TestCaller负责维护m_fixture的生命周期
TestMethod m_test;      // 指向某个test方法的函数指针

TestMethod的定义如下:

typedef void (Fixture::*TestMethod)();

正是因为有如上定义,才限制了TestCaller只能支持形参和返回值均为为void类型的test方法。

来看一下TestCaller的ctor和dtor,你会发现,对于Fixture的安置工作,CppUnit的实现者可谓细心周到:

// 由TestCaller创建fixture,负责维护其生命周期
TestCaller( std::string name, TestMethod test ) :
            TestCase( name ), 
            m_ownFixture( true ),
            m_fixture( new Fixture() ),
            m_test( test )
{
}

// 由外界传入fixture,TestCaller不负责维护其生命周期
TestCaller( std::string name, TestMethod test, Fixture& fixture) :
            TestCase( name ), 
            m_ownFixture( false ),
            m_fixture( &fixture ),
            m_test( test )
{
}

// 由外界传入fixture,由TestCaller负责维护其生命周期
TestCaller( std::string name, TestMethod test, Fixture* fixture) :
            TestCase( name ), 
            m_ownFixture( true ),
            m_fixture( fixture ),
            m_test( test )
{
}

// 根据m_ownFixture,决定是否销毁m_fixture
~TestCaller() 
{
  if (m_ownFixture)
    delete m_fixture;
}

作为TestCaller的基类,TestCase派生自Test和TestFixture,因此TestCaller有义务实现如下三个虚函数。

void setUp()
{
  // 简单地调用了m_fixture的setUp方法
  m_fixture->setUp ();
}

void tearDown()
{
  // 简单地调用了m_fixture的tearDown方法
  m_fixture->tearDown ();
}

void runTest()
{
  // 调用了m_fixture的一个test方法
  // 由此可见:
  // - test方法必须在Fixture类中定义
  // - 若Fixture类中存在多个test方法,则需一一建立与之对应的TestCaller
  try {
    (m_fixture->*m_test)();
  }
  catch ( ExpectedException & ) {
    return;
  }

  ExpectedExceptionTraits<ExpectedException>::expectedException();
}

这里不得不提到辅助类ExpectedExceptionTraits,还有前面出现过的NoExceptionExpected,它们和TestCaller一起被定以在同一个文件里。

template<typename ExceptionType>
struct ExpectedExceptionTraits
{
  static void expectedException()
  {
#if CPPUNIT_USE_TYPEINFO_NAME
    std::string message( "Expected exception of type " );
    message += TypeInfoHelper::getClassName( typeid( ExceptionType ) );
    message += ", but got none";
#else
    std::string message( "Expected exception but got none" );
#endif
    throw Exception( message );
  }
};

ExpectedExceptionTraits只有一个static方法,其唯一的作用是抛出一个Exception类型的异常,并附带一个说明信息,指出某个预计产生的异常并未出现,该预计的异常由ExpectedExceptionTraits的模板类型参数来指定。关于Exception,请见core部分。结合前面出现过的TestCaller::runTest的行为,我们可以得出如下结论:

通常情况下,如果调用m_fixture的test方法时,没有抛出任何异常,或者抛出的不是ExpectedException类型的异常,则ExpectedExceptionTraits的expectedException方法会产生一个异常来指出这一错误。其中的ExpectedException,由TestCaller的第二个类型参数来指定。

再看一下NoExceptionExpected的定义,它被作为TestCaller的第二个类型参数的缺省类型:

class CPPUNIT_API NoExceptionExpected
{
private:
  // 防止此类被实例化
  NoExceptionExpected();
};

什么事都不做!是的,NoExceptionExpected只是一个Marker class,它用来表明,TestCaller在任何时候都不会对运行test方法时所抛出的异常做预期性检查。当然,光有了NoExceptionExpected还不够,还需要定义一个ExpectedExceptionTraits的特化版本:

template<>
struct ExpectedExceptionTraits<NoExceptionExpected>
{
  static void expectedException()
  {
  }
};

同样是什么事都没做,再次结合TestCaller::runTest的行为,我们就可以得出如下结论:

当TestCaller的第二个类型参数为NoExceptionExpected时,如果调用m_fixture的test方法时

  • 没有抛出任何异常,则ExpectedExceptionTraits的expectedException方法(特化版)将被调用,并且不抛出任何异常;
  • 有异常被抛出,则一定不是NoExceptionExpected(因为那个private ctor),原样继续向外抛出此异常。

 

相关文件:AutoRegisterSuite.h

AutoRegisterSuite是一个模板类,其作用是自动注册指定类型的测试包,不过你不需要直接使用该类,而代之以如下的两个宏:

CPPUNIT_TEST_SUITE_REGISTRATION()
CPPUNIT_TEST_SUITE_NAMED_REGISTRATION()

关于宏,在随后的HelperMacros部分将会有更为详细的说明。

AutoRegisterSuite的全部内容是两个不同版本的ctor:

AutoRegisterSuite()
{
  // 利用TestSuiteFactory创建一个类工厂实例factory
  TestFactory *factory = new TestSuiteFactory<TestCaseType>();
  // 调用getRegistry(),在NamedRegistries中注册一个TestFactoryRegistry实例,
  // 注册项的名称为“All Tests”,并调用该实例的registerFactory,将factory注册为
  // 其下所属的一个类工厂实例
  TestFactoryRegistry::getRegistry().registerFactory( factory );
}

AutoRegisterSuite( const std::string &name )
{
  // 除了注册项的名称由外部指定外,其余同前
  TestFactory *factory = new TestSuiteFactory<TestCaseType>();
  TestFactoryRegistry::getRegistry( name ).registerFactory( factory );
}

这里的TestCaseType是AutoRegisterSuite的模板类型参数。前面曾经提到过的TestSuiteFactory,其makeTest方法会调用TestCaseType的suite方法以创建测试实例,至于makeTest的调用时机,则和TestFactoryRegistry的makeTest方法有关。

扩展部分(Extension)


在CppUnit中,除了提供基本的单元测试之外,还增加了很多扩展测试,比如:重复测试(RepeatedTest),正规测试(OrthodoxTest),这些内容都悉数收录在extension中。

[TestDecorator] [RepeatedTest] [Orthodox] [TestSetUp] [TestSucessListener] [TextTestProgressListener] [TextTestResult]

界面部分(TextUI)


这一部分主要提供了一个文本界面的测试运行环境(即以字符流方式输出到标准输出设备)。该测试环境在CppUnit中被称为test runner,对应的类是TestRunner。TestRunner可以运行所有测试,或者是其中的一个。下面的代码演示了如何使用TestRunner:

CppUnit::TextUi::TestRunner runner;
runner.addTest( ExampleTestCase::suite() );
runner.run( "", true );    // 空字串""代表运行所有的测试

在测试执行期间,TestRunner除了能输出最后的统计结果外,还可以打印输出跟踪信息。其中,跟踪信息的输出使用了TextTestProgressListener(见listener部分),统计结果的输出则使用了TextOutputter(见outputter部分)。当然,这些都是可选的。你可以在构造TestRunner期间或者随后通过调用setOutputter函数来指定其他类型的outputter。你也可以通过在eventManager()中注册其他TestListener,来定制跟踪信息。且看下面的示例:

CppUnit::TextUi::TestRunner runner;
runner.addTest( ExampleTestCase::suite() );

// 用CompilerOutputter代替TextOutputter
runner.setOutputter( CppUnit::CompilerOutputter::defaultOutputter(
    &runner.result(), std::cerr ) );
// 添加自定义的MyCustomProgressTestListener
MyCustomProgressTestListener progress;
runner.eventManager().addListener( &progress );

runner.run( "", true );    // 空字串""代表运行所有的测试

最后,TestRunner管理着其下所有测试对象的生命周期。

[TestRunner]

输出部分(Portability)


这一部分,通过若干参数的设定,解决了向不同平台移植时遇到的问题。另外还有一个叫做OStringStream的辅助类,不过morning以为,该类似乎置于helper部分更为合适。

[OStringStream] [其他]

附录(Appendix)——WIN32平台安装说明


目前,CPPUnit在WIN32平台下仅支持Microsoft Visual C++,而且你的VC++编译器至少应该是6.0版本的。

使用GUI TestRunner编译运行示例程序的步骤如下:

  1. 在VC++中打开examples/examples.dsw(包含所有的示例)
  2. 将HostApp设为active project
  3. 编译之
  4. 在VC中选择Tools/Customize.../Add-ins and Macro Files,点击Browse...
  5. 选择lib/TestRunnerDSPlugIn.dll文件,并按ok以注册该附加件(add-ins)
  6. 运行project

[Project创建结果]

框架 & 工具:

  • cppunit(cppunit.lib):单元测试的框架库,你将用它来编写单元测试。
  • cppunit_dll(cppunit_dll.dll/lib):同上,只是以DLL方式呈现。
  • TestRunner(testrunner.dll):一个MFC的扩展DLL,用来以GUI方式运行单元测试和查看结果。
  • DSPlugIn(lib/TestRunnerDSPlugIn.dll):一个VC++的附加件,为testrunner.dll所使用。有了它之后,你若在MFC TestRunner中双击某个failure,就会启动VC++,打开failure所在文件并定位到某行。
  • TestPlugInRunner:(警告:实验性的)一个VC++应用程序,用以运行测试插件。测试插件就是一个公开特定接口的DLL。该应用程序目前尚未完成(auto-reload特性丢失)。

所有库文件都被置于lib/目录下。

[示例]

  • CppUnitTestMain:一个实际的测试包(test suite)用来测试CppUnit。使用了TextTestRunner(文本方式的单元测试环境),利用CompilterOutputter进行post-build testing(即在编译结束之后紧跟着进行测试)。在配置中设定连接了cppunit的静态库和动态库。
  • CppUnitTestApp:包含了与CppUnitTestMain相同的测试包,但使用了MFC TestRunner(GUI方式的单元测试环境)
  • hierarchy : 一个演示如何子类化测试的例子(你也许更愿意使用HelperMacros.h以及宏CPPUNIT_TEST_SUB_SUITE,这种方式更为简洁清晰。本示例已经很久没有更新了)。
  • HostApp : 一个用MFC TestRunner演示各种失败测试的例子。也演示了MFC Unicode TestRunner。
  • TestPlugIn : 一个演示如何为TestPlugInRunner编写TestPlugIn的例子(实验性的).

[配置(Configuration)]

CppUnit和TestRunner带有3种配置。

  • Release():多线程DLL,release模式
  • Debug(d):Debug多线程DLL,debug模式
  • Unicode Release(u):Unicode多线程DLL,release模式
  • Unicode Debug(ud):Unicode Debug 多线程DLL,debug模式
  • Debug Crossplatform (cd): Debug 多线程DLL,没有使用type_info从类名中提取测试用例的包名。

对CppUnit而言,当创建dll时,字母“dll” 将被添加到后缀之后。

括号内的字母标明了添加到库名之后的后缀。例如,debug配置的cppunit静态库名为cppunitd.lib。debug配置的cppunit动态库名为cppunitd_dll.lib.

[创建(Building)]

  1. 在VC++中打开src/CppUnitLibraries.dsw工作区文件。
  2. 将TestPlugInRunner设为active project。
  3. 在'Build'菜单中选择'Batch Build...'
  4. 在Batch Build对话框中,选中所有的project 并按下build按钮。
  5. 所有的库文件可以在lib/目录下找到。

[测试(Testing)]

  1. 打开工作区文件examples/Examples.dsw。
  2. 将CppUnitTestApp设为active project.
  3. 为你要创建的库选择合适的配置。
  4. 编译运行project。TestRunner GUI将会出现。

[库(Libraries)]

所有编译后生成的库均可在'lib'目录中找到。多数库可以在src/CppUnitLibraries.dsw工作区中创建。

lib/:

  • cppunit.lib : CppUnit静态库“Multithreaded DLL”
  • cppunitd.lib : CppUnit静态库“Debug Multithreaded DLL”
  • cppunit_dll.dll : CppUnit动态库(DLL)“Multithreaded DLL”
  • cppunit_dll.lib : CppUnit动态导入库“Multithreaded DLL”
  • cppunitd_dll.dll : CppUnit动态库(DLL)“Debug Multithreaded DLL”
  • cppunitd_dll.lib : CppUnit动态导入库“Debug Multithreaded DLL”
  • qttestrunner.dll : QT TestRunner动态库(DLL)“Multithreaded DLL”
  • qttestrunner.lib : QT TestRunner导入库“Multithreaded DLL”
  • testrunner.dll : MFC TestRunner动态库(DLL)“Multithreaded DLL”
  • testrunner.lib : MFC TestRunner导入库“Multithreaded DLL”
  • testrunnerd.dll : MFC TestRunner动态库(DLL)“Debug Multithreaded DLL”
  • testrunnerd.lib : MFC TestRunner导入库“Debug Multithreaded DLL”
  • testrunneru.dll : MFC Unicode TestRunner动态库(DLL)“Multithreaded DLL”
  • testrunneru.lib : MFC Unicode TestRunner导入库“Multithreaded DLL”
  • testrunnerud.dll : MFC Unicode TestRunner动态库(DLL)“Debug Multithreaded DLL”
  • testrunnerud.lib : MFC Unicode TestRunner导入库“Debug Multithreaded DLL”
  • TestRunnerDSPlugIn.dll : 注册到你的VC++中的附加件。

注意:当你使用CppUnit DLL(cppunit*_dll.dll)时,你必须连接相关的导入库,并在project中定义预处理标识CPPUNIT_DLL。

[使用CppUnit]

  • 编写单元测试:

    为了编写单元测试,你需要连接cppunitXX.lib,此处的XX即所选配置对应的后缀字母。 你必须在你的project中打开RTTI开关(Project Settings/C++/C++ Language)。 CppUnit的include目录必须包含在include查找路径中。你可以通过在Project Settings/C++/Preprocessor/Additional include directories或者Tools/Options/Directories/Include中添加include目录做到这一点。

    简言之:

    • 打开RTTI开关
    • 连接lib/cppunitXX.lib
    • include/ 必须包含在include查找路径中

     

  • 使用TestRunner GUI:

    为了使用GUI的test runner,你需要连接testrunnerXX.lib和cppunitXX.lib,此处的XX即所选配置对应的后缀字母。 你必须在你的project中打开RTTI开关。 文件testrunner.dll必须位于你的应用程序所在的路径(Debug或Release目录,project的dsp文件所在目录,或环境变量PATH中所指定的目录)。 一个最简单的办法是,要么添加一个post-build命令,或者,将位于lib/目录下的testrunner.dll添加到你的project中来,并定制创建步骤,将dll文件拷贝到你的“中间结果”目录(通常是Debug或Release目录)。

    因为TestRunner GUI是一个MFC的扩展DLL,它能够访问当前应用程序的CWinApp。 参数设置将使用应用程序的注册键。这意味着,设置项“最近使用的测试”对每个应用程序而言都是不同的。

    简言之:

    • 打开RTTI开关
    • 连接lib/cppunitXX.lib和lib/testrunnerXX.lib
    • include/必须包含在include查找路径中
    • 为了运行你的project,lib/testrunnerXX.dll必须可用

     

  • 使用DSPlugIn:

    你必须在VC++中注册该插件。在Tools/Customize/Add-ins and Macro files中点击browse,并选择lib/TestRunnerDSPlugIn.dll(你可以注册release版或者debug版,都能运行)。

    若VC++正在运行,当你双击一个failure后,VC++将打开相关文件并定位到出错行。

  • 使用Test Plug In Runner:

    你的DLL必须导出(export)一个函数,该函数实现了在include/msvc6/testrunner/TestPlugInInterface.h中所定义的接口。作为范例,参见examples/msvc6/TestPlugIn/TestPlugInInterfaceImpl.*。注意:该runner仍处于实验阶段并未作足够多的测试。

    返回目录

相关文件:Portablility.h

其实OStringStream在先前很多地方都曾经出现过,比如:TestFactoryRegistry中、XmlOutputter中、TestAssert中。其作用是将整数转换为字符串并输出,功能上类似于C语言的itoa函数。事实上,从随CppUnit所附的ChangeLog中可以看到,先前正是用的itoa,只不过后来的一次refactoring中,才被OStringStream取代。其实现代码如下:

#if CPPUNIT_HAVE_SSTREAM
#   include <sstream>
    namespace CppUnit {
      class OStringStream : public std::ostringstream 
      {
      };
    }
#else 
#if CPPUNIT_HAVE_CLASS_STRSTREAM
#   include <string>
#   if CPPUNIT_HAVE_STRSTREAM
#       include <strstream>
#   else
#       include <strstream.h>
#   endif

    namespace CppUnit {
      class OStringStream : public std::ostrstream 
      {
      public:
          std::string str()
          {
            (*this) << '/0';
            std::string msg(std::ostrstream::str());
            std::ostrstream::freeze(false);
            return msg;
          }
      };
    }
#else
#   error Cannot define CppUnit::OStringStream.
#endif
#endif

 

相关文件:Portability.h,config-msvc6.h,config-bcb5.h

如前所述,CppUnit提供了一系列可供设置的参数(其实就是一系列宏定义),针对不同平台,你需要做出不同的选择。好在CppUnit的实现者为我们做了很多工作,使我们不用太多考虑这方面的问题,至少在Visual C++ 6.0和Borland C++ Builder 5.0这两个平台上是如此。Portability.h的开头有如下这样一段代码,它依据实际的语言平台(由特定的宏来指定),载入相应的参数设置文件:

#if defined(__BORLANDC__)
#    include <cppunit/config-bcb5.h>
#elif defined (_MSC_VER)
#    include <cppunit/config-msvc6.h>
#else
#    include <cppunit/config-auto.h>
#endif

这里的config-auto.h文件,morning并未在CppUnit的源码中找到,也许是程序作者的一时疏忽。至于在config-xxx.h中出现的那些参数,此处只列举一二,感兴趣的读者可自己去查看源码。

// helper部分的TypeInfoHelper中曾经出现过该宏
// 它表明在bcb和vc中函数std::string::compare的调用方式不一样

// config-msvc6.h
#ifdef CPPUNIT_FUNC_STRING_COMPARE_STRING_FIRST 
#undef CPPUNIT_FUNC_STRING_COMPARE_STRING_FIRST
#endif

// config-bcb5.h
#ifndef CPPUNIT_FUNC_STRING_COMPARE_STRING_FIRST 
#define CPPUNIT_FUNC_STRING_COMPARE_STRING_FIRST  1 
#endif

// 指明编译器是否支持c++的namespace语言特性
// bcb和vc均支持namespace

// config-msvc6.h
#ifndef CPPUNIT_HAVE_NAMESPACES 
#define CPPUNIT_HAVE_NAMESPACES  1 
#endif

// config-bcb5.h
#ifndef CPPUNIT_HAVE_NAMESPACES 
#define CPPUNIT_HAVE_NAMESPACES  1 
#endif

// OStringStream的定义中曾经出现过,指明是否存在<sstream>头文件,
// bcb和vc均包含有<sstream>头文件

// config-msvc6.h
#define CPPUNIT_HAVE_SSTREAM 1

// config-bcb5.h
#define CPPUNIT_HAVE_SSTREAM 1

此外还有一些,仅在某个平台下才会用到的参数,比如,以下内容均只在config-msvc6.h中出现:

// 忽略Debug符号大于255的警告
#if _MSC_VER > 1000     // VC++
#pragma warning( disable : 4786 )
#endif // _MSC_VER > 1000

// 若当前创建的是CppUnit的DLL库,则需要定义CPPUNIT_DLL_BUILD
#ifdef CPPUNIT_BUILD_DLL
#define CPPUNIT_API __declspec(dllexport)
#endif

// 若当前要链接到CppUnit的DLL库,则需要定义CPPUNIT_DLL
#ifdef CPPUNIT_DLL
#define CPPUNIT_API __declspec(dllimport)
#endif

最后,Portability.h中还定义了一些平台无关的参数,它们设定同样也影响着CppUnit的某些特性,比如:

// 若你希望使用传统风格的断言宏,比如:
// assert(), assertEqual()等等,则需将其设定为1
#ifndef CPPUNIT_ENABLE_NAKED_ASSERT
#define CPPUNIT_ENABLE_NAKED_ASSERT          0
#endif

// CPPUNIT_API是在<config_msvc6.h>中被定以的
// 若没有被定以,则表明程序不需要引用或生成CppUnit的DLL库
#ifndef CPPUNIT_API
#define CPPUNIT_API
#undef CPPUNIT_NEED_DLL_DECL
#define CPPUNIT_NEED_DLL_DECL 0
#endif

相关文件:TestRunner.h,TestRunner.cpp,TextTestRunner.h

TestRunner中定义了4个protected属性的成员变量:

TestSuite *m_suite;                     // 对应待运行的测试
TestResultCollector *m_result;          // 搜集测试结果(见output部分)
TestResult *m_eventManager;             // 收集测试过程中的相关信息
Outputter *m_outputter;                 // 输出测试统计结果

对于这几个成员变量的作用,注释中及前面部分已有提及。在这里,大家对TestResult和TestResultCollector的功能可能容易混淆。对于它们的区别以及TestResultCollector的“身世”,请见output部分。

TestRunner的ctor对这几个成员变量进行了初始化,若外部传入的outputter为空,则缺省创建TextOutputter对象赋给m_outputter,另外还调用了m_eventManager的addListener方法,将m_result作为一个兼听者加入其中。

TestRunner::TestRunner( Outputter *outputter ) 
    : m_outputter( outputter )
    , m_suite( new TestSuite( "All Tests" ) )
    , m_result( new TestResultCollector() )
    , m_eventManager( new TestResult() )
{
  if ( !m_outputter )
    m_outputter = new TextOutputter( m_result, std::cout );
  m_eventManager->addListener( m_result );
}

dtor则负责回收在ctor中所创建的资源:

TestRunner::~TestRunner()
{
  delete m_eventManager;
  delete m_outputter;
  delete m_result;
  delete m_suite;
}

正如前面所说,你可以在构造TestRunner之后,通过调用setOutputter函数来指定其他类型的outputter:

void TestRunner::setOutputter( Outputter *outputter )
{
  delete m_outputter;
  m_outputter = outputter;
}

TestRunner中的主要接口就是run方法,通过调用该函数才能运行测试:

bool TestRunner::run( std::string testName, 
        bool doWait, 
        bool doPrintResult, 
        bool doPrintProgress )
{
    runTestByName( testName, doPrintProgress );
    printResult( doPrintResult );
    wait( doWait );
    return m_result->wasSuccessful();
}

其中,testName是测试用例的名称,若为空则运行所有测试,否则运行指定测试;doWait为true时,表示在run函数返回之前,用户必须按一下RETURN键才能结束;doPrintResult为true时,测试结果将被输出到标准输出设备(前提是使用TextOutputter),否则不产生任何输出;doPrintProgress为true时,测试过程将被输出到标准输出设备(前提是使用TextTestProgressListener),否则不产生任何输出。

这里调用了runTestByName,请看源码及注释:

bool TestRunner::runTestByName( std::string testName,
    bool doPrintProgress )
{
  if ( testName.empty() )                       // 若testName为空则运行所有测试
    return runTest( m_suite, doPrintProgress );

  Test *test = findTestByName( testName );      // 否则查找指定测试
  if ( test != NULL )
    return runTest( test, doPrintProgress );    // 若成功找到则运行之

  std::cout << "Test " << testName << " not found." << std::endl;
  return false;
}

在runTestByName中,根据测试名称查找指定测试的任务落实到了findTestByName肩上:

Test *TestRunner::findTestByName( std::string name ) const
{
  for ( std::vector<Test *>::const_iterator it = m_suite->getTests().begin();
    it != m_suite->getTests().end();
    ++it )
  {
    Test *test = *it;
    if ( test->getName() == name )
      return test;
  }
  return NULL;
}

该函数以const_iterator遍历m_suite中的所有测试,寻找名字相符的测试。不过,从代码中可以看出,这里无法支持多层嵌套结构的测试集,这也算是一点遗憾吧。

找到指定测试之后,就可以将运行测试的任务转交给runTest方法了:

bool TestRunner::runTest( Test *test, bool doPrintProgress)
{
  TextTestProgressListener progress;
  if ( doPrintProgress )                // 若doPrintProgress为true则显示测试过程
    m_eventManager->addListener( &progress );

  test->run( m_eventManager );          // 此处才真正运行测试,m_eventManager就是TestResult

  if ( doPrintProgress )                // 若doPrintProgress为true则移除先前加入的progress
    m_eventManager->removeListener( &progress );
  return m_result->wasSuccessful();     // 返回测试结果成功与否
}

测试结束后,就可以通过调用printResult输出测试结果了:

void TestRunner::printResult( bool doPrintResult )
{
  std::cout << std::endl;
  if ( doPrintResult )
    m_outputter->write();
}

另外,TestRunner还提供了一个addTest方法,用以添加测试,其内部只是简单的调用了一下m_suite的addTest方法,相当简单:

void TestRunner::addTest( Test *test )
{
  if ( test != NULL )
    m_suite->addTest( test );
}

当然还少不了几个成员变量的getter方法:

TestResultCollector &TestRunner::result() const
{
  return *m_result;
}

TestResult &TestRunner::eventManager() const
{
  return *m_eventManager;
}

另一个相关的头文件TextTestRunner.h中,仅仅简单地做了一个typedef定义:

typedef CppUnit::TextUi::TestRunner TextTestRunner;

这就是TestRunner的大致内容。

相关文件:TestDecorator.h

它提供了一种方法,可以不用子类化Test类,同时又能扩展Test类的功能。我们可以派生TestDecorator,并用它来包装Test。其实这种方法是Decorator Pattern的一个应用,在GoF中对该pattern有如下描述:动态地给一个对象添加一些额外的职责。就增加功能来说,比生成子类更为灵活。

TestDecorator维护了一个指向Test实例的指针,并在ctor中设定。不过该实例的生命期,TestDecorator并不过问:

protected:
    Test        *m_test;

随后是四个public函数,其接口与Test的接口完全一致:

void        run             (TestResult *result);
int         countTestCases  () const;
std::string getName         () const;
std::string toString        () const;

函数的实现就是简单的调用m_test的对应接口:

inline int TestDecorator::countTestCases () const
{ return m_test->countTestCases (); }

inline void TestDecorator::run (TestResult *result)
{ m_test->run (result); }


inline std::string TestDecorator::toString () const
{ return m_test->toString (); }


inline std::string TestDecorator::getName () const
{ return m_test->getName(); }

在TestDecorator的派生类中,这些功能将得到扩展。

 

相关文件:RepeatedTest.h,RepeatedTest.cpp

派生自TestDecorator,其功能是对测试重复运行指定的次数(类似于某种强度测试)。private成员变量m_timesRepeat记录了重复的次数:

private:
    const int m_timesRepeat;

该值在ctor中设定:

RepeatedTest( Test *test,int timesRepeat ) : 
    TestDecorator( test ), 
    m_timesRepeat(timesRepeat) {}

这里的test参数,就是所要执行的测试,可能是某个测试用例,也可能是测试包。

随后是函数countTestCases、run和toString的子类化版本:

// 返回本次测试中测试用例的总数
// 总数 = 实际总数 * 重复次数
int RepeatedTest::countTestCases() const
{
  return TestDecorator::countTestCases () * m_timesRepeat;
}

std::string RepeatedTest::toString() const
{
  return TestDecorator::toString () + " (repeated)";
}

// 运行测试
// 重复调用基类的run,并在基类中调用m_test的run方法
void RepeatedTest::run( TestResult *result )
{
  for ( int n = 0; n < m_timesRepeat; n++ )
  {
    if ( result->shouldStop() )
      break;
    TestDecorator::run( result );
  }
}

 

相关文件:Orthodox.h

该类实现了正规测试的功能。它派生自TestCase,是一个模板类,有一个类型参数ClassUnderTest,代表将要运行的测试。所谓正规测试,就是对待测类(即ClassUnderTest)执行一组简单的测试,确保其至少具有如下基本操作:

  • default ctor
  • operator==和operator!=
  • assignment(即operator=)
  • operator!
  • safe passage(即copy ctor)

若其中任何一项没有通过测试,则模板类就不会实例化。否则,实例化后将检查这些操作的语义是否正确。当你需要确认一组待测类具有相同表现时,采用被模板化的测试用例非常有用,Orthodox就是一个很好的例子。可以想见,在实际工作中,我们也可以效仿Orthodox的做法,从而“扩展”CppUnit以适应自己的特定环境。

以下代码演示了如何将一个复数类的正规测试添加到测试包中:

TestSuite *suiteOfTests = new TestSuite;
suiteOfTests->addTest (new ComplexNumberTest ("testAdd");
suiteOfTests->addTest (new TestCaller<Orthodox<Complex> > ());  // 非常简单

来看一下Orthodox的定义:

template <typename ClassUnderTest> class Orthodox : public TestCase
{
public:
  Orthodox () : TestCase ("Orthodox") {}

protected:
  ClassUnderTest  call (ClassUnderTest object);
  void            runTest ();
};

唯一需要解释的就是runTest方法,Orthodox是如何检查ClassUnderTest是否符合要求的呢:

template <typename ClassUnderTest>
void Orthodox<ClassUnderTest>::runTest ()
{
  // 确保default ctor被定义,否则无法通过编译
  ClassUnderTest   a, b, c;

  // 确保operator==被定义,否则无法通过编译
  // 同时检查operator==的语义
  CPPUNIT_ASSERT (a == b);

  // 确保operator!、operator=和operator!=被定义
  // 否则无法通过编译
  // 同时检查operator!=的语义
  b.operator= (a.operator! ());
  CPPUNIT_ASSERT (a != b);

  // 检查operator!和operator==的语义
  b = !!a;
  CPPUNIT_ASSERT (a == b);

  b = !a;

  // 以下检查copy ctor是否被定义及其语义正确与否
  c = a;
  CPPUNIT_ASSERT (c == call (a));
  c = b;
  CPPUNIT_ASSERT (c == call (b));
}

这里的call是辅助函数,“迫使”编译器调用copy ctor,以检查safe passage:

template <typename ClassUnderTest>
ClassUnderTest Orthodox<ClassUnderTest>::call (ClassUnderTest object)
{
  return object;
}

所有的奥妙就在上面这几行代码中。

 

相关文件:TestSetUp.h,TestSetUp.cpp

同样派生自TestDecorator,它使测试类具有了SetUp和TearDown的特性。关于这两个特性请见core部分的TestFixture。

该类定义了两个protected属性的虚函数,以供派生类覆盖:

protected:
  virtual void setUp();
  virtual void tearDown();

此外,就是子类化了run方法:

void TestSetUp::run( TestResult *result )
{
  setUp();
  TestDecorator::run(result);
  tearDown();
}

兼听者部分(Listener)


这部分较为简单,主要根据具体需求,提供了两个TestListener的派生类,它们分别用在不同的场合。

 

相关文件:TestSucessListener.h,TestSucessListener.cpp

派生自TestListener和SynchronizedObject(多重继承),兼具两者特性。作为一个实际的Observer,接收来自TestResult的信息,用以“监听”测试是否成功。关于TestListener、SynchronizedObject以及TestResult请见core部分的说明。

TestSucessListener内部所持有的成员变量m_sucess标示了测试成功与否,至于究竟如何“监听”,不妨来看一下TestSucessListener的相关实现:

TestSucessListener::TestSucessListener( SynchronizationObject *syncObject )
    : SynchronizedObject( syncObject )
    , m_sucess( true )
{
}

在ctor中,m_sucess被初始化为true。至于syncObject,则提供了同步功能,为后续调用ExclusiveZone提供便利。

void TestSucessListener::addFailure( const TestFailure &failure )
{
  ExclusiveZone zone( m_syncObject );
  m_sucess = false;
}

在测试发生错误时,TestResult将会调用TestSucessListener的addFailure,后者只是简单地将m_sucess设置为false。TestSucessListener只关心测试成功与否,至于有关测试结果的详细情况则由TestResultCollector负责“监听”,关于TestResultCollector请见output部分。因为要考虑多线程环境,所以用到了ExclusiveZone,这也是为什么TestSucessListener需要继承SynchronizedObject的原因(别忘了ExclusiveZone是protected属性的)。m_syncObject就是前面ctor中提到的syncObject。

void TestSucessListener::reset()
{
  ExclusiveZone zone( m_syncObject );
  m_sucess = true;
}

顾名思义,在测试运行之前reset内部状态,将m_sucess置为true。

最后,为外部提供测试成功与否的查询接口也是必不可少的:

bool TestSucessListener::wasSuccessful() const
{
  ExclusiveZone zone( m_syncObject );  // [此处是read操作,似不必劳驾ExclusiveZone]
  return m_sucess;
}

 

相关文件:TextTestProgressListener.h,TextTestProgressListener.cpp

派生自TestListener,用来“监听”测试用例的运行状态,并将结果定向到标准错误输出设备(即屏幕)。作为一个简易的文本流方式的结果输出工具,TextTestProgressListener已是绰绰有余了。

// 继承自基类的虚函数,在测试运行前被调用
void TextTestProgressListener::startTest( Test *test )
{
  std::cerr << ".";
  std::cerr.flush();
}

// 继承自基类的虚函数,运行测试失败时被调用
void TextTestProgressListener::addFailure( const TestFailure &failure )
{
  std::cerr << ( failure.isError() ? "E" : "F" );
  std::cerr.flush();
}

// 在测试运行后被调用[疑为endTest,也许是作者的疏忽]
void TextTestProgressListener::done()
{
  std::cerr  <<  std::endl;
  std::cerr.flush();
}

 

相关文件:TextTestResult.h,TextTestResult.cpp

以文本流方式输出测试运行的结果。不过该类在新版本中已被标上了“DEPRECATED”,并被TextTestProgressListener和TextOutputter(在outputter部分讲解)所取代。因为是不推荐使用的,所以此处不准备细述了,感兴趣的读者可以自己看。

记得2003年的春节假期,难得有时间可以静下来充充电,于是有了研读CppUnit源码的念头。一来是为了熟悉CppUnit的使用环境,而来也是希望通过研读源码汲取有益的东西,这一系列的文章便是整理自笔者当初的源码阅读笔记。

如何将诸多技术综合运用到一个实际的framework中来,笔者以为,CppUnit为我们提供了一个难易适中的参考范例。这应该是一个很好的例子,因为它不甚复杂,却汇聚了一个framework所必需的某些设计思想以及实现技巧。在这里,我们可以看到STL的实际使用(包括一些简单的traits技法),Design Pattern的灵活运用(比如:Composite,Factory,Decorator,Singleton,Observer等)。

当然,也应该指出,由于CppUnit还在不断改进中,其代码中未免还有“败笔”及不尽如人意之处。但是,瑕不掩瑜,并且从中我们也可以感受到一个成熟框架的演进过程。

由于有过一点framework的设计经验和体会,笔者在阅读CppUnit源码的过程中,时常能有共鸣,并且对于框架的设计者在某些细节的处理方法,也深以为然,偶尔也有“英雄所见略同”的感叹。希望可以通过笔者的讲解,使大家也能够同样有亲历之感。

 

CppUnit是xUnit系列中的c++实现版本,它是从JUnit移植过来的,第一个移植版本由Michael Feathers完成,相关信息可以在http://www.xprogramming.com/software.htm找到。它是操作系统相关的,随后,Jerome Lacoste将之移植到了Unix/Solaris,在上述连接中也能找到该版本的相关信息。CppUnit项目就是基于这些版本建立起来的。有关CppUnit的讨论可以在http://c2.com/cgi/wiki?CppUnit找到,在那里你还可以找到CppUnit先前的版本以及许多其它操作系统环境下的移植版本。这个库受GNU LGPL(Lesser General Public License)的保护。作者包括:Eric Sommerlade (sommerlade@gmx.net),Michael Feathers (mfeathers@objectmentor.com),Jerome Lacoste (lacostej@altern.org),J.E. Hoffmann ,Baptiste Lepilleur ,Bastiaan Bakker ,Steve Robbins

这里所选用的是CppUnit 1.8.0版,你可以从http://sourceforge.net/projects/cppunit/下载到最新版本。

 

作为一个完整的CppUnit framework,虽然源码所在的实际路径可能不尽相关,但从逻辑上讲它们被划为如下几个部分:

  • core:CppUnit的核心部分
  • output:掌管结果输出
  • helper:一些辅助类
  • extension:作为单元测试的延伸,对CppUnit core部分的扩展(比如:常规测试,重复测试)
  • listener:监视测试进程和测试结果
  • textui:一个运行单元测试的文本环境
  • portability:提供针对不同平台的移植设置

上述所有的内容均被置于CppUnit名字空间之内。

 

  • 本文主要内容依据CppUnit源码而来,部分内容还来自于源码自身所附的注释、ChangeLog等
  • 本文只作源码解读,至于xUnit家族的相关背景及基本知识笔者不准备叙述,读者可以参看相关文章
  • 对于文中所涉及的Design Pattern,Refactoring,STL等相关知识,请读者参看相关资料。
  • 除了文章本身,文中所列源码,也夹带了morning的一些注释,用以进一步说明代码意图,注释中方括号内为morning的疑问
  • 为了节省篇幅、简化内容、突出主题,文中未列出全部代码,而是有选择的给出部分代码
  • 由于工作的缘故,撰写这一系列的文章是陆续进行的,因此文字斟酌、行文的前后一致性方面不甚考究,在此请诸位见谅。如有必要且时间允许,morning将会对此作一完整的整理。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值