当前位置: 首页 > 工具软件 > CppTest > 使用案例 >

cpptest_了解CppTest

阮梓
2023-12-01

本文是有关用于单元测试的开源工具的系列文章中的最后一篇,详细介绍了Niklas Lundell CppTest的功能强大的框架CppTestCppTest的最大CppTest是它易于理解,易于适应和使用。 了解如何使用CppTest创建单元测试和测试套件,测试治具设计并自定义回归日志格式,同时熟悉几个有用的CppTest宏。 如果您是高级用户,本文还提供了CppUnitCppTest框架之间的比较。

安装及使用

CppTest可从Sourceforge上免费下载(参见相关主题 ),GNU宽通用公共许可证(GPL)下。 构建源代码遵循通常的开源configure-make格式。 结果是一个名为libcpptest的静态库。 客户端代码必须包括头文件cppTest.h(它是已下载源的一部分),以及指向静态库libcpptest.a的链接。 本文基于CppTest版本1.1.0。

什么是测试套件?

单元测试旨在测试源代码的特定部分。 以最简单的形式,此测试涉及一组C/C++函数,这些函数测试其他C/C++代码。 CppTestTest名称空间中定义了一个名为Suite的类,该类提供基本的测试套件功能。 需要用户定义的测试套件,通过定义用作实际单元测试的功能来扩展此功能。 清单1定义了一个名为myTest的类,该类具有两个函数,每个函数都测试一段源代码。 TEST_ADD是用于注册测试的宏。

清单1.扩展基本的Test :: Suite类
#include “cppTest.h”

class myTest : public Test::Suite { 
  void function1_to_test_some_code( );
  void function2_to_test_some_code( );

  myTest( ) { 
      TEST_ADD (myTest::function1_to_test_some_code); 
      TEST_ADD (myTest::function2_to_test_some_code); 
  } 
};

扩展测试套件

您可以轻松扩展测试套件的功能以创建测试套件的层次结构。 之所以需要执行此类操作,是因为每个测试套件都可能测试某个特定的代码区域(例如,用于编译器的解析,代码生成,代码优化的测试套件),并且层次结构可以随着时间的推移更好地进行管理。 。 清单2显示了如何创建这样的层次结构。

清单2.创建单元测试层次结构
#include “cppTest.h”

class unitTestSuite1 : public Test::Suite { … } 
class unitTestSuite2 : public Test::Suite { … } 

class myTest : public Test::Suite { 
  myTest( ) { 
      add (std::auto_ptr<Test::Suite>(new unitTestSuite1( ))); 
      add (std::auto_ptr<Test::Suite>(new unitTestSuite2( ))); 
  } 
};

add方法属于Suite类。 清单3显示了它的原型(来自标头cpptest-suite.h)。

清单3. Suite :: add方法声明
class Suite
{
 public:	
    …
    void add(std::auto_ptr<Suite> suite);
    ...
} ;

运行第一个测试

Suite类的run方法负责执行测试。 清单4显示了测试运行。

清单4.以详细模式运行测试套件
#include “cppTest.h”

class myTest : public Test::Suite { 
  void function1_to_test_some_code( ) { … };
  void function2_to_test_some_code( ) { … };

  myTest( ) { 
      TEST_ADD (myTest::function1_to_test_some_code); 
      TEST_ADD (myTest::function2_to_test_some_code); 
  } 
}; 

int main ( ) 
{ 
  myTest tests; 
  Test::TextOutput output(Test::TextOutput::Verbose);
  return tests.run(output);
}

仅当所有单个单元测试都成功时, run方法才返回设置为True的布尔值。 run方法的参数是TextOutput类型的对象。 TextOutput类处理打印测试日志。 默认情况下,日志转储到屏幕上。

除了详细模式外,还有简洁模式。 两种模式之间的区别在于,详细模式打印行号/文件名信息,以在各个测试中断言失败,而简洁模式仅提供通过或失败测试的计数。

失败后继续

那么,如果单个测试失败会怎样? 是否继续的决定完全取决于客户端代码。 默认行为是继续执行其他测试。 清单5显示了run方法的原型。

清单5. run方法的原型
bool Test::Suite::run(	
    Output &   output,
    bool 	        cont_after_fail = true	 
);

对于在检测到第一个失败后必须退出回归的情况, run方法的第二个参数需要为False。 但是,将第二个标志设置为False可能并不明显。 假定客户端代码正在尝试将信息写入已满100%的磁盘:代码将失败,并且该套件中所有具有类似行为的进一步测试也将失败。 在这种情况下,立即停止回归是有意义的。

了解输出格式化程序

输出格式化程序背后的想法是,您可能需要使用不同格式的回归运行报告:文本,HTML等。 因此, run方法本身不会转储结果,而是接受Output类型的对象,该对象负责显示结果。 CppTest提供了三种不同类型的输出格式化程序:

  • Test::TextOutput 这是所有输出处理程序中最简单的。 显示模式可以是冗长或简洁。
  • Test::CompilerOutput 以类似于编译器生成日志的方式生成输出。
  • Test::HtmlOutput 精美HTML输出。

默认情况下,所有三个格式化程序都将其输出转储到std::cout 。 前两个格式化程序的构造函数接受类型为std::ostream的参数,该参数指示应将输出转储到何处(例如,在文件中以备将来使用)。 您也可以选择创建输出格式化程序的自定义版本。 为此,唯一的要求是用户定义的格式化程序应从Test::Output派生。 要了解不同的输出格式,请考虑清单6中的代码。

清单6.在简洁模式下运行TEST_FAIL宏
#include “cppTest.h”

class failTest1 : public Test::Suite { 
void always_fail( ) { 
  TEST_FAIL (“This always fails!\n”); 
} 
public: 
  failTest1( ) { TEST_ADD(failTest1::always_fail); } 
}; 

int main ( ) { 
  failTest1 test1;
  Test::TextOutput output(Test::TextOutput::Terse);
  return test1.run(output) ? 1 : 0;
}

请注意, TEST_FAILTEST_FAIL中的预定义宏,它会导致断言失败。 (稍后会有更多信息。)清单7显示了输出。

清单7.仅显示失败计数的简洁输出
failTest1: 1/1, 0% correct in 0.000000 seconds
Total: 1 tests, 0% correct in 0.000000 seconds

清单8显示了在详细模式下运行相同代码时的输出。

清单8.详细输出,显示文件/行信息,消息,测试套件信息等
failTest1: 1/1, 0% correct in 0.000000 seconds
        Test:    always_fail
        Suite:   failTest1
        File:     /home/arpan/test/mytest.cpp
        Line:    5
        Message: "This always fails!\n"

Total: 1 tests, 0% correct in 0.000000 seconds

接下来,清单9显示了使用编译器样式格式的代码。

清单9.以编译器样式的输出格式运行TEST_FAIL宏
#include “cppTest.h”

class failTest1 : public Test::Suite { 
void always_fail( ) { 
  TEST_FAIL (“This always fails!\n”); 
} 
public: 
  failTest1( ) { TEST_ADD(failTest1::always_fail); } 
}; 

int main ( ) { 
  failTest1 test1;
  Test::CompilerOutput output;
  return test1.run(output) ? 1 : 0;
}

注意语法与清单10所示的GNU编译器集合(GCC)生成的编译日志相似。

清单10.仅显示失败计数的简洁输出
/home/arpan/test/mytest.cpp:5: “This always fails!\n”

默认情况下,编译器格式的输出为GCC样式的构建日志。 但是,也可以使用Microsoft®Visual C ++®和Borland编译器格式获得输出。 清单11生成了Visual C ++样式的日志,该日志转储到了输出文件中。

清单11.以编译器样式的输出格式运行TEST_FAIL宏
#include <ostream>

int main ( ) { 
  failTest1 test1;
  std::ofstream ofile;
  ofile.open("test.log");
  Test::CompilerOutput output(
      Test::CompilerOutput::MSVC, ofile);
 return test1.run(output) ? 1 : 0;
}

清单12显示了清单11中的代码执行后的文件test.log的内容。

清单12.虚拟C ++风格的编译器输出
/home/arpan/test/mytest.cpp (5) :  “This always fails!\n”

最后,下面是所有这些示例中最出色的: HtmlOutput用法的代码。 请注意,HTML格式化程序在构造函数中不接受文件句柄,而是依靠generate方法。 generate方法的第一个参数是类型为std::ostream的对象,其默认值为std::cout (有关详细信息,请参见源头文件cpptest-htmloutput.h)。 您可以使用文件句柄将日志重定向到其他位置。 清单13提供了一个示例。

清单13. HTML样式的格式
#include *<ostream>

int main ( ) { 
  failTest1 test1;
  std::ofstream ofile;
  ofile.open("test.log");
  Test::HtmlOutput output( );
  test1.run(output); 
  output.generate(ofile);
  return 0;
}

清单14显示了在test.log中生成的部分HTML输出。

清单14.生成HTML输出的摘录
…
<table summary="Test Failure" class="table_result">
  <tr>
    <td style="width:15%" class="tablecell_title">Test</td>
    <td class="tablecell_success">failTest1::always_fail</td>
  </tr>
  <tr>
    <td style="width:15%" class="tablecell_title">File</td>
    <td class="tablecell_success">/home/arpan/test/mytest.cpp:18</td>
  </tr>
  <tr>
    <td style="width:15%" class="tablecell_title">Message</td>
    <td class="tablecell_success">"This always fails!\n"</td>
  </tr>
</table>
…

关于测试治具

构成同一测试套件一部分的单元测试通常具有相同的初始化要求集:必须创建具有某些参数的对象,打开文件句柄/操作系统端口等。 解决每个问题的更好方法是使用一些通用的初始化和退出例程,而不是在每个类方法中重复相同的代码,而这些例程是针对每个测试调用的。 您必须在他的测试套件中定义设置和拆卸方法。 清单15定义了使用夹具的测试套件myTestWithFixtures

清单15.用夹具创建一个测试套件
#include “cppTest.h”

class myTestWithFixtures : public Test::Suite { 
  void function1_to_test_some_code( );
  void function2_to_test_some_code( );

  public: 
  myTest( ) { 
      TEST_ADD (function1_to_test_some_code); 
      TEST_ADD (function2_to_test_some_code); 
  } 

  protected: 
    virtual void setup( ) { … };
    virtual void tear_down( ) { … };
};

请注意,您无需显式调用setup和tear-down方法。 除非您计划在以后扩展测试套件,否则无需将这些例程声明为虚拟的。 这两个例程的返回类型都必须为void并且不接受任何参数。

在CppTest中学习宏

CppTest提供了一些有用的宏,您可以在测试客户端源代码的实际方法中使用它们。 这些宏在cpptest-assert.h(包含在cpptest.h中)内部定义。 接下来描述一些宏及其潜在的用例。 请注意,除非另有说明,否则显示的输出是使用详细模式提供的。

TEST_FAIL(消息)

如清单16所示,该宏意味着无条件失败。 您可以使用此宏的一种典型情况是处理客户端函数的结果。 如果结果与任何预期结果不一致,则将引发异常和消息。 当TEST_FAIL宏被命中时,将不再执行该特定单元测试中的其他代码。

清单16.使用TEST_FAIL宏的客户端代码
void myTestSuite::unitTest1 ( ) { 
    int result = usercode( );
    switch (result) { 
       case 0: // Do Something 
       case 1: // Do Something 
       …
       default: TEST_FAIL (“Invalid result\n”); 
   }
}

TEST_ASSERT(表达式)

该宏类似于C断言库例程,除了TEST_ASSERT在调试和发行版本中TEST_ASSERT起作用。 如果该表达式被验证为False,则标记一个错误。 清单17显示了此宏的内部实现。

清单17. TEST_ASSERT宏实现
#define TEST_ASSERT(expr)  \
{  	\
  if (!(expr))  \
  { \
  assertment(::Test::Source(__FILE__, __LINE__, #expr));	\
     if (!continue_after_failure()) return;  \
  } \
}

TEST_ASSERT_MSG(表达式,消息)

该宏与TEST_ASSERT相似,除了断言失败时,该消息代替输出中的表达式而显示。 这是带有或不带有消息的断言。

TEST_ASSERT (1 + 1 == 0);
TEST_ASSERT (1 + 1 == 0, “Invalid expression”);

清单18显示了命中此断言时的输出。

清单18. TEST_ASSERT和TEST_ASSERT_MSG宏的输出
Test:    compare
Suite:   CompareTestSuite
File:     /home/arpan/test/mytest.cpp
Line:    91
Message: 1 + 1 == 0

Test:    compare
Suite:   CompareTestSuite
File:     /home/arpan/test/mytest.cpp
Line:    92
Message: Invalid Expression

TEST_ASSERT_DELTA(表达式1,表达式2,增量)

如果expression1和expression2之差超过delta,则会引发异常。 当expression1和expression2是浮点数时,此宏特别有用-例如,取决于实际舍入的方式,可以将4.3存储为4.299999或4.300001,这样,进行比较就需要一个增量。 另一个例子是测试操作系统I / O的代码:每次打开文件所花费的时间可能并不相同,但是必须在一定范围内。

TEST_ASSERT_DELTA_MSG(表达式,消息)

该宏与TEST_ASSERT_DELTA宏相似,除了断言失败时还会发出一条消息。

TEST_THROWS(表达式,异常)

该宏验证表达式并期望出现异常。 如果没有捕获异常,则触发断言。 请注意,表达式的实际值不会进行任何测试:这是正在测试的异常。 考虑清单19中的代码。

清单19.处理整数异常
class myTest1 : public Test::Suite { 
  …
  void func1( ) { 
     TEST_THROWS (userCode( ), int);
  }
  public: 
     myTest1( ) { TEST_ADD(myTest1::func1); } 
}; 

void userCode( ) throws(int) { 
   …
   throw int;
}

请注意, userCode例程的返回类型无关紧要:它也可以是双userCode或整数。 因为userCode在这里无条件地抛出了int类型的异常,所以测试可以userCode通过。

TEST_THROWS_ANYTHING(表达式)

有时,客户端例程会根据情况抛出不同种类的异常。 要处理这种情况,您可以使用TEST_THROWS_ANYTHING宏,该宏未指定预期的异常类型。 只要在执行客户端代码后捕获到某些异常,就不会触发该断言。

TEST_THROWS_MSG(表达式,异常,消息)

该宏类似于TEST_THROWS ,除了在这种情况下,消息是打印的,而不是表达式。 考虑以下代码:

TEST_THROWS(userCode( ), int);
TEST_THROWS(userCode( ), int, “No expected exception of type int”);

清单20显示了这些断言失败时的输出。

清单20. TEST_THROWS和TEST_THROWS_MSG宏的输出
Test:    func1
Suite:   myTest1
File:    /home/arpan/test/mytest.cpp
Line:    24
Message: userCode()

Test:    func2
Suite:   myTest1
File:    /home/arpan/test/mytest.cpp
Line:    32
Message: No expected exception of type int

CppUnit和CppTest之间的比较

本系列的第2部分讨论了CppUnit ,这是开源单元测试框架的另一种流行选择。 CppTest是一个比CppUnit简单得多的框架,但是它可以完成工作。 这是这两个功能强大的工具相互配合的快速比较:

  • 易于创建单元测试和测试套件。 CppUnitCppTest创建类方法的单元测试,该类本身是从某些工具提供的Test类派生的。 但是, CppTest的语法稍微简单一些,因为测试注册发生在类构造函数内部。 对于CppUnit ,需要其他宏CPPUNIT_TEST_SUITECPPUNIT_TEST_SUITE_ENDS
  • 运行测试。 CppTest只是在测试套件上调用run方法,而CppUnit使用一个单独的TestRunner类,其run方法被调用以运行测试。
  • 扩展测试层次结构。 对于CppTest ,总是可以通过创建一个继承自旧类的新类来扩展先前的测试套件。 新类将定义一些添加到单元测试池中的附加功能。 您只需在新类类型的对象上调用run方法。 相反, CppUnit要求您将CPPUNIT_TEST_SUB_SUITE宏与类继承一起使用才能实现相同的效果。
  • 生成格式化的输出。 CppTestCppUnit都可以自定义输出。 但是,尽管CppTest具有有用的,预定义HTML输出格式化程序,但CppUnit没有。 但是, CppUnit仅支持XML格式。 两者都支持文本和编译器样式格式。
  • 创建测试夹具。 要使用测试装置, CppUnit要求测试类从CppUnit::TestFixture派生。 您必须提供设置和拆卸程序的定义。 对于CppTest ,您仅需要提供设置和拆卸程序的定义。 这绝对是一个更好的解决方案,因为它可以简化客户端代码。
  • 预定义的实用程序宏支持。 CppTestCppUnit都具有一组可比较的宏,用于断言,处理浮点等等。
  • 头文件。 CppTest要求您包含一个头文件,而CppUnit客户端代码必须包括多个头,例如HelperMacros.h和TextTestRunner.h,具体取决于所使用的功能。

结论

单元测试是开发当今软件的基础,而CppTestC/C++开发人员中用于处理代码测试,从而避免维护麻烦的另一个工具。 有趣的是,这个由三部分组成的系列文章介绍的所有三个工具CppTest测试框架CppUnitCppTest使用相同的基本概念,例如固定装置,断言的宏和输出格式化程序。 每个工具都是开源的,您可以轻松地根据需要进一步自定义代码(例如, CppTest的XML输出CppTest )。 你在等什么?


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

 类似资料: