全面了解cppunit_了解CppUnit

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

下载并安装CppUnit

为了本文的目的,我将CppUnit下载并安装在具有g ++-3.2.3和make-3.79.1的Linux®计算机(内核2.4.21)上。 安装简单且标准:运行configure命令,然后执行makemake 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_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宏接受新类及其超类作为两个参数。 在客户端,您只需注册新类而不是两个类。 就是这样:其余语法与创建测试套件几乎相同。

使用夹具定制测试

夹具或CppUnit上下文中的TestFixture旨在为各个测试提供干净的设置和退出例程。 要使用灯具,可以从CppUnit::TestFixture派生测试类,并覆盖预定义的setUptearDown方法。 在执行单元测试之前,将调用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的命名空间的两个类: TestSuiteTestCaller ,在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

运行多个测试套件

您可以创建多个测试套件,并在单个操作中使用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对象。

最后,您的输出发生了什么? TextTestRunnerrun方法之后一直显示很多信息,但是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框架的某些特定类: TestResultTestListenerTestRunnerCompilerOutputter等。 作为独立的单元测试框架,CppUnit提供了更多功能。 CppUnit中有一些类可用于XML输出生成( XMLOutputter )和以GUI模式运行测试( MFCTestRunnerQtTestRunner ),以及一个插件接口( CppUnitTestPlugIn )。 确保进一步了解CppUnit文档的类层次结构以及安装随附的示例。


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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值