开放源码 C/C++ 单元测试工具,第 2 部分: 了解 CppUnit
Arpan Sen 是致力于电子设计自动化行业的软件开发首席工程师。他使用各种 UNIX 版本(包括 Solaris、SunOS、HP-UX、IRIX,以及 Linux 和 Microsoft Windows)已经多年。他热衷于各种软件性能优化技术、图论和并行计算。Arpan 获得了软件系统硕士学位。
简介: 在讨论开放源码单元测试实用程序的 系列文章 的第 2 篇中,介绍 JUnit 测试框架的 C++ 版本 CppUnit。
查看本系列更多内容
发布日期: 2010 年 2 月 25 日 级别: 中级 其他语言版本: 英文 访问情况 2040 次浏览 建议: 0 (添加评论 )
本文是讨论开放源码单元测试工具的 系列文章 的第 2 篇,介绍非常受欢迎的 CppUnit — 最初由 Eric Gamma 和 Kent Beck 开发的 JUnit 测试框架的 C++ 版本。C++ 版本由 Michael Feathers 创建,它包含许多类,有助于进行白盒测试和创建自己的回归测试套件。本文介绍一些比较有用的 CppUnit 特性,比如 TestCase、TestSuite、TestFixture、TestRunner 和辅助宏。
下载和安装 CppUnit
对于本文,我在一台 Linux® 机器(内核 2.4.21)上用 g++-3.2.3 和 make-3.79.1 下载并安装了 CppUnit。安装过程很简单,是标准的:运行 configure 命令,然后运行 make 和 make 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_SUITE 和 CPPUNIT_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 上下文中,fixture 或 TestFixture 用于为各个测试提供简洁的设置和退出例程。要想使用 fixture,测试类应该派生自 CppUnit::TestFixture 并覆盖预先定义的 setUp 和 tearDown 方法。在执行单元测试之前调用 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 名称空间中的两个类:TestSuite 和 TestCaller(分别在 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 还有很多其他有用的方法,比如可以使用 printStatistics 和 printFailureReport 获取它转储的信息的子集。
回页首
更多定制:跟踪测试时间
到目前为止,都是默认使用 TextTestRunner 运行测试。这种方式非常简便:实例化一个 TextTestRunner 类型的对象,在其中添加测试和输出器,然后调用 run 方法。现在,我们使用 TestRunner(TextTestRunner 的超类)和一种称为监听器 的类改变这种运行过程。假设希望跟踪各个测试花费的时间 — 执行性能基准测试的开发人员常常需要这样做。在进一步解释之前,先看一下 清单 20 。这段代码使用三个类 TestRunner、TestResult 和 myListener(派生自 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 的子类。需要覆盖 startTest 和 endTest 方法,这两个方法分别在每个测试之前和之后执行。可以通过扩展这些方法轻松地检查各个测试花费的时间。那么,为什么不在设置/清除例程中添加这种功能呢?可以这么做,但是这意味着在每个测试套件的设置/清除方法中会出现重复的代码。
接下来,看看运行器对象,它是 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 框架的一些类:TestResult、TestListener、TestRunner、CompilerOutputter 等。CppUnit 是一个独立的单元测试框架,它还提供许多其他功能。CppUnit 中有用于生成 XML 输出的类(XMLOutputter)和用于以 GUI 模式运行测试的类(MFCTestRunner 和 QtTestRunner),还提供一个插件接口(CppUnitTestPlugIn)。一定要查阅 CppUnit 文档来了解它的类层次结构,通过示例了解详细的安装信息。
参考资料
学习
获得产品和技术
下载 CppUnit :获取 CppUnit 的最新版本。 IBM 产品评估版 :试用来自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的应用程序开发工具和中间件产品。 讨论
关于作者
Arpan Sen 是致力于电子设计自动化行业的软件开发首席工程师。他使用各种 UNIX 版本(包括 Solaris、SunOS、HP-UX、IRIX,以及 Linux 和 Microsoft Windows)已经多年。他热衷于各种软件性能优化技术、图论和并行计算。Arpan 获得了软件系统硕士学位。