开放源码 C/C++ 单元测试工具,第 2 部分: 了解 CppUnit

在讨论开放源码单元测试实用程序的 系列文章 的第 2 篇中,介绍 JUnit 测试框架的 C++ 版本 CppUnit。

Arpan Sen, 资深技术人员, Mentor Graphics

Arpan Sen 是致力于电子设计自动化行业的软件开发首席工程师。他使用各种 UNIX 版本(包括 Solaris、SunOS、HP-UX、IRIX,以及 Linux 和 Microsoft Windows)已经多年。他热衷于各种软件性能优化技术、图论和并行计算。Arpan 获得了软件系统硕士学位。



2010 年 2 月 25 日

本文是讨论开放源码单元测试工具的 系列文章 的第 2 篇,介绍非常受欢迎的 CppUnit — 最初由 Eric Gamma 和 Kent Beck 开发的 JUnit 测试框架的 C++ 版本。C++ 版本由 Michael Feathers 创建,它包含许多类,有助于进行白盒测试和创建自己的回归测试套件。本文介绍一些比较有用的 CppUnit 特性,比如 TestCase、TestSuite、TestFixture、TestRunner 和辅助宏。

常用缩写词

  • GUI:图形用户界面
  • XML:可扩展标记语言

下载和安装 CppUnit

对于本文,我在一台 Linux® 机器(内核 2.4.21)上用 g++-3.2.3 和 make-3.79.1 下载并安装了 CppUnit。安装过程很简单,是标准的:运行 configure 命令,然后运行 makemake install。注意,对于 cygwin 等平台,这个过程可能无法顺利地完成,所以一定要通过 INSTALL-unix 文档了解详细的安装信息。如果安装成功,应该会在安装路径(CPPUNIT_HOME)中看到 CppUnit 的 include 和 lib 文件夹。清单 1 给出文件夹结构。

清单 1. CppUnit 安装目录结构
[arpan@tintin] echo $CPPUNIT_HOME
/home/arpan/ibm/cppUnit
[arpan@tintin] ls $CPPUNIT_HOME
bin  include  lib  man  share

要想编译使用 CppUnit 的测试,必须构建源代码:

g++ <C/C++ file> -I$CPPUNIT_HOME/include –L$CPPUNIT_HOME/lib -lcppunit

注意,如果是使用 CppUnit 的共享库版本,可能需要使用 –ldl 选项编译源代码。安装之后,还可能需要修改 UNIX® 环境变量 LD_LIBRARY_PATH 以反映 libcppunit.so 的位置。


使用 CppUnit 创建基本测试

学习 CppUnit 的最佳方法是创建一个叶级测试(leaf level test)。CppUnit 附带一整套预先定义的类,可以用它们方便地设计测试。为了保持连续性,先回顾一下本系列 第 1 部分 中讨论过的字符串类(见 清单 2)。

清单 2. 简单的字符串类
#ifndef _MYSTRING
#define _MYSTRING

class mystring { 
  char* buffer; 
  int length;
  public: 
    void setbuffer(char* s) { buffer = s; length = strlen(s); } 
    char& operator[ ] (const int index) { return buffer[index]; }
    int size( ) { return length; }
 }; 

#endif

与字符串相关的典型检查包括检查空字符串的长度是否为 0 以及访问范围超出索引是否导致错误消息/异常。清单 3 使用 CppUnit 执行这些测试。

清单 3. 字符串类的单元测试
#include <cppunit/TestCase.h>
#include <cppunit/ui/text/TextTestRunner.h>

class mystringTest : public CppUnit::TestCase {
public:

  void runTest() {
    mystring s;
    CPPUNIT_ASSERT_MESSAGE("String Length Non-Zero", s.size() != 0);
  }
};

int main ()
{
  mystringTest test;
  CppUnit::TextTestRunner runner;
  runner.addTest(&test);

  runner.run();
  return 0;
}

要学习的第一个 CppUnit 类是 TestCase。要想为字符串类创建单元测试,需要创建 CppUnit::TestCase 类的子类并覆盖 runTest 方法。定义了测试本身之后,实例化 TextTestRunner 类,这是一个控制器类,必须在其中添加测试(vide addTest 方法)。清单 4 给出 run 方法的输出。

清单 4. 清单 3 中代码的输出
[arpan@tintin] ./a.out
!!!FAILURES!!!
Test Results:
Run:  1   Failures: 1   Errors: 0


1) test:  (F) line: 26 try.cc
assertion failed
- Expression: s.size() == 0
- String Length Non-Zero

为了确认断言确实起作用了,把 CPPUNIT_ASSERT_MESSAGE 宏中的条件改为相反的条件。清单 5 给出条件改为 s.size() ==0 之后代码的输出。

清单 5. 条件改为 s.size( ) == 0 之后清单 3 中代码的输出
[arpan@tintin] ./a.out

OK (1 tests)

注意,TestRunner 并非运行单一测试或测试套件的惟一方法。CppUnit 还提供另一个类层次结构 — 即模板化的 TestCaller 类。可以不使用 runTest 方法,而是使用 TestCaller 类执行任何方法。清单 6 提供一个小示例。

清单 6. 使用 TestCaller 运行测试
class ComplexNumberTest ... { 
  public: 
     void ComplexNumberTest::testEquality( ) { … } 
};

CppUnit::TestCaller<ComplexNumberTest> test( "testEquality", 
                                             &ComplexNumberTest::testEquality );
CppUnit::TestResult result;
test.run( &result );

在上面的示例中,定义了一个类型为 ComplexNumberText 的类,其中包含 testEquality 方法(测试两个复数是否相等)。用这个类对 TestCaller 进行模板化,与使用 TestRunner 时一样,通过调用 run 方法执行测试。但是,这样使用 TestCaller 类意义不大:TextTestRunner 类会自动显示输出。而在使用 TestCaller 时,必须使用另一个类处理输出。在本文后面使用 TestCaller 类定义定制的测试套件时,您会看到这种代码。


使用断言

清单 7. CPPUNIT_ASSERT_MESSAGE 的定义
#define CPPUNIT_ASSERT_MESSAGE(message,condition)                          \
  ( CPPUNIT_NS::Asserter::failIf( !(condition),                            \
                                  CPPUNIT_NS::Message( "assertion failed", \
                                                       "Expression: "      \
                                                       #condition,         \
                                                       message ),          \
                                  CPPUNIT_SOURCELINE() ) )

清单 8 给出这个断言使用的 failIf 方法的声明。

清单 8. failIf 方法的声明
struct Asserter
{
…
  static void CPPUNIT_API failIf( bool shouldFail,
                                  const Message &message,
                                  const SourceLine &sourceLine = SourceLine() );
…
}

如果 failIf 方法中的条件为真,就会抛出一个异常。run 方法在内部处理该过程。另一个有意思、有用的宏是 CPPUNIT_ASSERT_DOUBLES_EQUAL,它使用一个容差值检查两个双精度数是否相等(即 |expected – actual | ≤ delta)。清单 9 给出宏定义。

清单 9. CPPUNIT_ASSERT_DOUBLES_EQUAL 宏定义
void CPPUNIT_API assertDoubleEquals( double expected,
                                     double actual,
                                     double delta,
                                     SourceLine sourceLine,
                                     const std::string &message );
#define CPPUNIT_ASSERT_DOUBLES_EQUAL(expected,actual,delta)        \
  ( CPPUNIT_NS::assertDoubleEquals( (expected),            \
                                    (actual),              \
                                    (delta),               \
                                    CPPUNIT_SOURCELINE(),  \
                                    "" ) )

再次测试字符串类

为了测试 mystring 类的其他方面,可以在 runTest 方法中添加更多检查。但是,这么做很快就会变得难以管理了,除非是最简单的类。这时就需要定义和使用测试套件。清单 10 为字符串类定义一个测试套件。

清单 10. 为字符串类定义测试套件
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TextTestRunner.h>
#include <cppunit/extensions/HelperMacros.h>

class mystringTest : public CppUnit::TestCase {
public:
  void checkLength() {
    mystring s;
    CPPUNIT_ASSERT_MESSAGE("String Length Non-Zero", s.size() == 0);
  }

  void checkValue() {
    mystring s;
    s.setbuffer("hello world!\n");
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Corrupt String Data", s[0], 'w');
  }

  CPPUNIT_TEST_SUITE( mystringTest );
  CPPUNIT_TEST( checkLength );
  CPPUNIT_TEST( checkValue );
  CPPUNIT_TEST_SUITE_END();
};

这很简单。使用 CPPUNIT_TEST_SUITE 宏定义测试套件。mystringTest 类中的方法形成测试套件中的单元测试。我们稍后研究这些宏及其内容,但是先看看使用这个测试套件的客户机代码(见 清单 11)。

清单 11. 使用 mystring 类的测试套件的客户机代码
CPPUNIT_TEST_SUITE_REGISTRATION ( mystringTest );

int main ()
{
  CppUnit::Test *test =
    CppUnit::TestFactoryRegistry::getRegistry().makeTest();
  CppUnit::TextTestRunner runner;
  runner.addTest(test);

  runner.run();
  return 0;
}

清单 12 给出运行 清单 11 时的输出。

清单 12. 清单 10 和清单 11 中代码的输出
[arpan@tintin] ./a.out
!!!FAILURES!!!
Test Results:
Run:  2   Failures: 2   Errors: 0


1) test: mystringTest::checkLength (F) line: 26 str.cc
assertion failed
- Expression: s.size() == 0
- String Length Non-Zero


2) test: mystringTest::checkValue (F) line: 32 str.cc
equality assertion failed
- Expected: h
- Actual  : w
- Corrupt String Data

CPPUNIT_ASSERT_EQUAL_MESSAGE 的定义在头文件 TestAssert.h 中,它检查预期参数和实际参数是否匹配。如果不匹配,就显示指定的消息。在 HelperMacros.h 中定义的 CPPUNIT_TEST_SUITE 宏可以简化创建测试套件并在其中添加测试的流程。在内部创建一个 CppUnit::TestSuiteBuilderContext 类型的模板化对象(这是 CppUnit 上下文中的测试套件),每个 CPPUNIT_TEST 调用在套件中添加相应的类方法。类方法作为代码的单元测试。请注意宏的次序:编译各个 CPPUNIT_TEST 宏的代码必须在 CPPUNIT_TEST_SUITECPPUNIT_TEST_SUITE_END 宏之间。


组织新测试

随着时间的推移,开发人员会不断添加功能,这些功能也需要测试。在同一测试套件中不断添加测试会逐渐造成混乱,而且对首次测试的修改容易随着修改的不断增加而丢失。好在 CppUnit 提供一个有用的 CPPUNIT_TEST_SUB_SUITE 宏,可以使用它扩展现有的测试套件。清单 13 使用这个宏。

清单 13. 扩展测试套件
class mystringTestNew : public mystringTest {
public:
  CPPUNIT_TEST_SUB_SUITE (mystringTestNew, mystringTest);
  CPPUNIT_TEST( someMoreChecks );
  CPPUNIT_TEST_SUITE_END();

  void someMoreChecks() {
    std::cout << "Some more checks...\n";
  }
};

CPPUNIT_TEST_SUITE_REGISTRATION ( mystringTestNew );

注意,新的类 mystringTestNew 是从前面的 myStringTest 类派生的。CPPUNIT_TEST_SUB_SUITE 宏的两个参数是新的类和它的超类。在客户端,只注册这个新类,不需要注册两个类。语法的其他部分与创建测试套件的语法相同。


使用 fixtures 定制测试

在 CppUnit 上下文中,fixtureTestFixture 用于为各个测试提供简洁的设置和退出例程。要想使用 fixture,测试类应该派生自 CppUnit::TestFixture 并覆盖预先定义的 setUptearDown 方法。在执行单元测试之前调用 setUp 方法,在测试执行完时调用 tearDown清单 14 演示如何使用 TestFixture

清单 14. 使用测试 fixture 定制测试套件
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TextTestRunner.h>
#include <cppunit/extensions/HelperMacros.h>

class mystringTest : public CppUnit::TestFixture {
public:
  void setUp() { 
     std::cout << “Do some initialization here…\n”;
  }

  void tearDown() { 
      std::cout << “Cleanup actions post test execution…\n”;
  }

  void checkLength() {
    mystring s;
    CPPUNIT_ASSERT_MESSAGE("String Length Non-Zero", s.size() == 0);
  }

  void checkValue() {
    mystring s;
    s.setbuffer("hello world!\n");
    CPPUNIT_ASSERT_EQUAL_MESSAGE("Corrupt String Data", s[0], 'w');
  }

  CPPUNIT_TEST_SUITE( mystringTest );
  CPPUNIT_TEST( checkLength );
  CPPUNIT_TEST( checkValue );
  CPPUNIT_TEST_SUITE_END();
};

清单 15 给出 清单 14 中代码的输出。

清单 15. 清单 14 中代码的输出
[arpan@tintin] ./a.out
. Do some initialization here…
FCleanup actions post test execution…
. Do some initialization here…
FCleanup actions post test execution…

!!!FAILURES!!!
Test Results:
Run:  2   Failures: 2   Errors: 0


1) test: mystringTest::checkLength (F) line: 26 str.cc
assertion failed
- Expression: s.size() == 0
- String Length Non-Zero


2) test: mystringTest::checkValue (F) line: 32 str.cc
equality assertion failed
- Expected: h
- Actual  : w
- Corrupt String Data

正如在输出中看到的,每次执行单元测试都会显示设置和清除例程消息。


创建不使用宏的测试套件

可以创建不使用任何辅助宏的测试套件。这两种风格并没有明显的优劣,但是无宏风格的代码更容易调试。要想创建不使用宏的测试套件,应该实例化 CppUnit::TestSuite,然后在套件中添加测试。最后,把套件本身传递给 CppUnit::TextTestRunner,然后再调用 run 方法。客户端代码很相似,见 清单 16

清单 16. 创建不使用辅助宏的测试套件
int main ()
{
  CppUnit::TestSuite* suite = new CppUnit::TestSuite("mystringTest");
  suite->addTest(new CppUnit::TestCaller<mystringTest>("checkLength",
                &mystringTest::checkLength));
  suite->addTest(new CppUnit::TestCaller<mystringTest>("checkValue",
                &mystringTest::checkLength));

  // client code follows next 
  CppUnit::TextTestRunner runner;
  runner.addTest(suite);

  runner.run();
  return 0;
}

要想理解 清单 16,需要理解 CppUnit 名称空间中的两个类:TestSuiteTestCaller(分别在 TestSuite.h 和 TestCaller.h 中声明)。在执行 runner.run() 调用时,对于每个 TestCaller 对象,在 CppUnit 内部调用 runTest 方法,它进而调用传递给 TestCaller<mystringTest> 构造函数的例程。清单 17 中的代码(取自 CppUnit 源代码)说明如何为每个套件调用测试。

清单 17. 执行套件中的测试
void
TestComposite::doRunChildTests( TestResult *controller )
{
  int childCount = getChildTestCount();
  for ( int index =0; index < childCount; ++index )
  {
    if ( controller->shouldStop() )
      break;

    getChildTestAt( index )->run( controller );
  }
}

TestSuite 类派生自 CppUnit::TestComposite

理解 CppUnit 中的指针

一定要在堆上声明测试套件,因为 CppUnit 在内部在 TestRunner 销毁函数中删除 TestSuite 指针。但是,这可能不是最好的设计决策,而且在 CppUnit 文档中未被提及。


运行多个测试套件

可以创建多个测试套件并使用 TextTestRunner 在一个操作中运行它们。只需像 清单 16 那样创建每个测试套件,然后使用 addTest 方法把它们添加到 TextTestRunner 中,见 清单 18

清单 18. 使用 TextTestRunner 运行多个套件
CppUnit::TestSuite* suite1 = new CppUnit::TestSuite("mystringTest");
suite1->addTest(…);
…
CppUnit::TestSuite* suite2 = new CppUnit::TestSuite("mymathTest");
…
suite2->addTest(…);
CppUnit::TextTestRunner runner;
runner.addTest(suite1);
runner.addTest(suite2);
…

定制输出的格式

到目前为止,测试的输出都是由 TextTestRunner 类默认生成的。但是,CppUnit 允许使用定制的输出格式。用于实现这个功能的类之一是 CompilerOutputter(在头文件 CompilerOutputter.h 中声明)。这个类允许指定输出中文件名-行号信息的格式。另外,可以把日志直接保存到文件中,而不是发送到屏幕。清单 19 提供一个把输出转储到文件的示例。注意格式 %p:%l:前者表示文件的路径,后者表示行号。使用这种格式时的典型输出像 /home/arpan/work/str.cc:26 这样。

清单 19. 把测试输出转发到日志文件并采用定制的格式
#include <cppunit/extensions/TestFactoryRegistry.h>
#include <cppunit/ui/text/TextTestRunner.h>
#include <cppunit/extensions/HelperMacros.h>
#include <cppunit/CompilerOutputter.h>

int main ()
{
  CppUnit::Test *test =
    CppUnit::TestFactoryRegistry::getRegistry().makeTest();
  CppUnit::TextTestRunner runner;
  runner.addTest(test);

  const std::string format("%p:%l");
  std::ofstream ofile;
  ofile.open("run.log");
  CppUnit::CompilerOutputter* outputter = new
    CppUnit::CompilerOutputter(&runner.result(), ofile);
  outputter->setLocationFormat(format);
  runner.setOutputter(outputter);

  runner.run();
  ofile.close();
  return 0;
}

CompilerOutputter 还有很多其他有用的方法,比如可以使用 printStatisticsprintFailureReport 获取它转储的信息的子集。


更多定制:跟踪测试时间

到目前为止,都是默认使用 TextTestRunner 运行测试。这种方式非常简便:实例化一个 TextTestRunner 类型的对象,在其中添加测试和输出器,然后调用 run 方法。现在,我们使用 TestRunnerTextTestRunner 的超类)和一种称为监听器 的类改变这种运行过程。假设希望跟踪各个测试花费的时间 — 执行性能基准测试的开发人员常常需要这样做。在进一步解释之前,先看一下 清单 20。这段代码使用三个类 TestRunnerTestResultmyListener(派生自 TestListener)。这里仍然使用 清单 10 中的 mystringTest 类。

清单 20. TestListener 类的使用
class myListener : public CppUnit::TestListener {
public:
  void startTest(CppUnit::Test* test) {
    std::cout << "starting to measure time\n";
  }
  void endTest(CppUnit::Test* test) {
    std::cout << "done with measuring time\n";
  }
};

int main ()
{
  CppUnit::TestSuite* suite = new CppUnit::TestSuite("mystringTest");
  suite->addTest(new CppUnit::TestCaller<mystringTest>("checkLength",
                &mystringTest::checkLength));
  suite->addTest(new CppUnit::TestCaller<mystringTest>("checkValue",
                &mystringTest::checkLength));

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

  myListener listener;
  CppUnit::TestResult result;
  result.addListener(&listener);

  runner.run(result);
  return 0;
}

清单 21 给出 清单 20 的输出。

清单 21. 清单 20 中代码的输出
	[arpan@tintin] ./a.out
starting to measure time
done with measuring time
starting to measure time
done with measuring time

myListener 类是 CppUnit::TestListener 的子类。需要覆盖 startTestendTest 方法,这两个方法分别在每个测试之前和之后执行。可以通过扩展这些方法轻松地检查各个测试花费的时间。那么,为什么不在设置/清除例程中添加这种功能呢?可以这么做,但是这意味着在每个测试套件的设置/清除方法中会出现重复的代码。

接下来,看看运行器对象,它是 TestRunner 类的实例,它在 run 方法中接收一个 TestResult 类型的参数,并在 TestResult 对象中添加监听器。

最后,输出结果会发生什么变化?TextTestRunner 在运行 run 方法之后显示许多信息,但是 TestRunner 不显示这些信息。我们需要使用输出器对象显示监听器对象在执行测试期间收集的信息。清单 22 显示需要对 清单 20 做的修改。

清单 22. 添加输出器以显示测试执行信息
runner.run(result);
CppUnit::CompilerOutputter outputter( &listener, std::cerr );
outputter.write();

但是等一下:代码还无法编译。CompilerOutputter 的构造函数需要一个 TestResultCollector 类型的对象,而且因为 TestResultCollector 本身派生自 TestListener(关于 CppUnit 类层次结构的详细信息见 参考资料),所以需要从 TestResultCollector 派生 myListener清单 23 给出可编译的代码。

清单 23. 从 TestResultCollector 派生监听器类
class myListener : public CppUnit::TestResultCollector {
…
};

int main ()
{
  …

  myListener listener;
  CppUnit::TestResult result;
  result.addListener(&listener);

  runner.run(result);

  CppUnit::CompilerOutputter outputter( &listener, std::cerr );
  outputter.write();

  return 0;
}

输出见 清单 24

清单 24. 清单 23 中代码的输出
[arpan@tintin] ./a.out
starting to measure time
done with measuring time
starting to measure time
done with measuring time
str.cc:31:Assertion
Test name: checkLength
assertion failed
- Expression: s.size() == 0
- String Length Non-Zero

str.cc:31:Assertion
Test name: checkValue
assertion failed
- Expression: s.size() == 0
- String Length Non-Zero

Failures !!!
Run: 0   Failure total: 2   Failures: 2   Errors: 0

结束语

本文主要讨论了 CppUnit 框架的一些类:TestResultTestListenerTestRunnerCompilerOutputter 等。CppUnit 是一个独立的单元测试框架,它还提供许多其他功能。CppUnit 中有用于生成 XML 输出的类(XMLOutputter)和用于以 GUI 模式运行测试的类(MFCTestRunnerQtTestRunner),还提供一个插件接口(CppUnitTestPlugIn)。一定要查阅 CppUnit 文档来了解它的类层次结构,通过示例了解详细的安装信息。

参考资料

学习

获得产品和技术

  • 下载 CppUnit:获取 CppUnit 的最新版本。
  • IBM 产品评估版:试用来自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的应用程序开发工具和中间件产品。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=AIX and UNIX
ArticleID=469703
ArticleTitle=开放源码 C/C++ 单元测试工具,第 2 部分: 了解 CppUnit
publish-date=02252010