开放源码 C/C++ 单元测试工具,第 2 部分: 了解 CppUnit
2017-07-12 00:01
423 查看
https://www.ibm.com/developerworks/cn/aix/library/au-ctools2_cppunit/index.html
本文是讨论开放源码单元测试工具的
系列文章 的第 2 篇,介绍非常受欢迎的 CppUnit — 最初由 Eric Gamma 和 Kent Beck 开发的 JUnit 测试框架的
XML:可扩展标记语言
1 给出文件夹结构。
清单 1. CppUnit 安装目录结构
要想编译使用 CppUnit 的测试,必须构建源代码:
注意,如果是使用 CppUnit 的共享库版本,可能需要使用
回页首
清单 2)。
清单 2. 简单的字符串类
与字符串相关的典型检查包括检查空字符串的长度是否为 0 以及访问范围超出索引是否导致错误消息/异常。清单 3 使用 CppUnit 执行这些测试。
清单 3. 字符串类的单元测试
要学习的第一个 CppUnit 类是
清单 4. 清单 3 中代码的输出
为了确认断言确实起作用了,把
清单 5. 条件改为 s.size( ) == 0 之后清单 3 中代码的输出
注意,
清单 6. 使用 TestCaller 运行测试
在上面的示例中,定义了一个类型为
回页首
清单 8 给出这个断言使用的
清单 8. failIf 方法的声明
如果
清单 9. CPPUNIT_ASSERT_DOUBLES_EQUAL 宏定义
回页首
清单 10. 为字符串类定义测试套件
这很简单。使用
11)。
清单 11. 使用 mystring 类的测试套件的客户机代码
清单 12 给出运行清单
11 时的输出。
清单 12. 清单 10 和清单 11 中代码的输出
回页首
13 使用这个宏。
清单 13. 扩展测试套件
注意,新的类
回页首
清单 14. 使用测试 fixture 定制测试套件
清单 15 给出清单
14 中代码的输出。
清单 15. 清单 14 中代码的输出
正如在输出中看到的,每次执行单元测试都会显示设置和清除例程消息。
回页首
清单 16。
清单 16. 创建不使用辅助宏的测试套件
要想理解
清单 16,需要理解 CppUnit 名称空间中的两个类:
清单 17. 执行套件中的测试
回页首
清单 16 那样创建每个测试套件,然后使用
清单 18. 使用 TextTestRunner 运行多个套件
回页首
19 提供一个把输出转储到文件的示例。注意格式
清单 19. 把测试输出转发到日志文件并采用定制的格式
回页首
20。这段代码使用三个类
清单 10 中的
清单 20. TestListener 类的使用
清单 21 给出清单
20 的输出。
清单 21. 清单 20 中代码的输出
接下来,看看运行器对象,它是
最后,输出结果会发生什么变化?
20 做的修改。
清单 22. 添加输出器以显示测试执行信息
但是等一下:代码还无法编译。
清单 23. 从 TestResultCollector 派生监听器类
输出见
清单 24。
清单 24. 清单 23 中代码的输出
回页首
和
本文是讨论开放源码单元测试工具的
系列文章 的第 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命令,然后运行
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 文档来了解它的类层次结构,通过示例了解详细的安装信息。
相关文章推荐
- 开放源码 C/C++ 单元测试工具,第 2 部分: 了解 CppUnit
- 开放源码 C/C++ 单元测试工具,第 1 部分: 了解 Boost 单元测试框架
- 开放源码 C/C++ 单元测试工具,第 1 部分: 了解 Boost 单元测试框架
- 开放源码 C/C++ 单元测试工具,第 1 部分: 了解 Boost 单元测试框架
- 开放源码 C/C++ 单元测试工具,第 1 部分: 了解 Boost 单元测试框架
- 开放源码 C/C++ 单元测试工具,第 1 部分: 了解 Boost 单元测试框架
- C++单元测试工具CppUnit入门
- 短小精悍的C++单元测试框架CppUnitLite源码分析
- C++单元测试工具CppUnit使用简介
- 单元测试工具googletest C++Test和CppUnit
- C++单元测试工具CppUnit使用简介 【转载】
- C++单元测试工具 -- CppUnit
- C++单元测试工具CppUnit使用简介
- C++单元测试工具之CPPUnit使用
- C++单元测试工具CppUnit使用简介 【转载】
- C++单元测试工具CppUnit入门
- 铁公鸡拔毛!Google准备开放部分源码
- SWT 和 JFace, 第 2 部分: 简介 了解菜单、列表、组合框、表和树
- Ajax 改造,第 2 部分: 使用 jQuery、Ajax、工具提示和 lightbox 改进现有站点
- WebSphere Application Server 中的内存泄漏检测与分析: 第 2 部分:用于泄漏检测与分析的工具和功能