全面了解cppunit

    技术2024-07-04  96

    本文是有关用于单元测试的开放源代码工具系列的第二篇文章,介绍了非常流行的CppUnit-JUnit测试框架的C++端口,该端口最初由Eric Gamma和Kent Beck开发。 C++端口是由Michael Feathers创建的,它包含各种类,可以帮助白盒测试和创建自己的回归套件。 本文介绍了一些更有用的CppUnit功能,例如TestCase,TestSuite,TestFixture,TestRunner和帮助程序宏。

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

    下载并安装CppUnit

    为了本文的目的,我将CppUnit下载并安装在具有g ++-3.2.3和make-3.79.1的Linux®计算机(内核2.4.21)上。 安装简单且标准:运行configure命令,然后执行make和make install 。 请注意,对于某些平台(例如cygwin),此过程可能无法顺利运行,因此请务必查看安装随附的INSTALL-unix文档以获取详细信息。 如果安装成功,则应该在安装路径中看到CppUnit的include和lib文件夹,我们将其称为CPPUNIT_HOME。 清单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的最佳方法是创建叶级测试。 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.清单3中条件为s.size()== 0的代码的输出
    [arpan@tintin] ./a.out OK (1 tests)

    请注意, TestRunner不是运行单个测试或测试套件的唯一方法。 CppUnit提供了备用的类层次结构(即,模板化的TestCaller类)来运行测试。 可以使用TestCaller类代替runTest方法来执行任何方法。 清单6提供了一个小示例。

    清单6.使用TestCaller运行测试
    class ComplexNumberTest ... { public: void ComplexNumberTest::testEquality( ) { … } }; CppUnit::TestCaller<ComplexNumberTest> test( "testEquality", &ComplexNumberTest::testEquality ); CppUnit::TestResult result; test.run( &result );

    在上面的示例中,使用方法testEquality (测试两个复数的相等性)定义了ComplexNumberText类型的类。 TestCaller该类对TestCaller进行模板化,就像TestRunner ,您可以调用run方法来执行测试。 但是,按原样使用TestCaller类没有多大用处: TextTestRunner类自动处理输出的显示。 对于TestCaller ,您必须使用单独的类来处理输出。 当您使用TestCaller类定义自定义测试套件时,将在本文后面看到这种类型的代码流。

    使用断言

    CppUnit为最常见的断言场景提供了几个例程。 这些被定义为CppUnit::Asserter类的公共静态方法,可在标头Asserter.h中使用。 标头TestAssert.h中也为大多数这些类提供了预定义的宏。 在清单2中 ,这是CPPUNIT_ASSERT_MESSAGE的定义方式(请参见清单7 ):

    清单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方法中的条件变为True,则将引发异常。 run方法在内部处理此过程。 另一个有趣且有用的宏是CPPUNIT_ASSERT_DOUBLES_EQUAL ,它使用容差值(因此|expected – actual | ≤ delta CPPUNIT_ASSERT_DOUBLES_EQUAL )检查两个双精度数是否相等。 清单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宏接受新类及其超类作为两个参数。 在客户端,您只需注册新类而不是两个类。 就是这样:其余语法与创建测试套件几乎相同。

    使用夹具定制测试

    夹具或CppUnit上下文中的TestFixture旨在为各个测试提供干净的设置和退出例程。 要使用灯具,可以从CppUnit::TestFixture派生测试类,并覆盖预定义的setUp和tearDown方法。 在执行单元测试之前,将调用setUp方法,在执行测试时,将调用tearDown 。 清单14显示了如何使用TestFixture 。

    清单14.用测试夹具制作一个测试套件
    #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 ,然后将单个测试添加到套件中。 最后,在调用run方法之前,将套件本身传递给CppUnit::TextTestRunner 。 客户端代码几乎相同,如清单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()调用时,CppUnit的内部将为每个单独的TestCaller对象调用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派生的( 有关详细信息,请参阅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文档的类层次结构以及安装随附的示例。


    翻译自: https://www.ibm.com/developerworks/aix/library/au-ctools2_cppunit/index.html

    相关资源:微信小程序源码-合集6.rar
    Processed: 0.021, SQL: 9