当前位置:主页   - 电脑 - 程序设计 - C/C++
开放源码 C/C++ 单元测试工具,第 3 部分: 了解 CppTest
来源:网络   作者:Arpan Sen   更新时间:2011-09-01
收藏此页】    【字号    】    【打印】    【关闭

  本文是讨论开放源码单元测试工具的系列的最后一篇,详细介绍 Niklas Lundell 开发的强大框架 CppTest。CppTest 最大的优点是容易理解、掌握和使用。学习如何使用 CppTest 创建单元测试和测试套件、设计测试装备和定制回归测试日志格式,熟悉 CppTest 提供的几个宏。对于高级用户,本文还提供 CppUnit 和 CppTest 框架之间的对比。

  常用缩略词

  HTML: 超文本标记语言

  I/O: 输入/输出

  XML: 可扩展标记语言

  安装和使用

  可以从 Sourceforge 免费下载 CppTest,采用的许可协议是 GNU Lesser General Public License (GPL)。按一般的开放源码配置-构建过程构建源代码。这会生成一个名为 libcpptest 的静态库。客户机代码必须包含下载的源代码中的头文件 cppTest.h,还要包含静态库 libcpptest.a 的链接。本文使用 CppTest version 1.1.0。

  什么是测试套件?

  单元测试用于测试源代码的特定部分。形式最简单的测试套件包含一组测试其他 C/C++ 代码的 C/C++ 函数。CppTest 在 Test 名称空间中定义一个名为 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); 
 } 
}; 

编缉推荐阅读以下文章

  • 开放源码 C/C++ 单元测试工具,第 2 部分: 了解 CppUnit
  • 开放源码 C/C++ 单元测试工具,第 1 部分: 了解 Boost 单元测试框架
  • C++单元测试工具CppUnit使用简介
  • 单元测试工具C++ Test简介

  扩展测试套件

  您可以很容易扩展测试套件的功能,创建测试套件的层次结构。每个测试套件可能测试不同的代码部分,例如分别测试编译器的解析、代码生成和代码优化,创建层次结构有助于更好地管理。清单 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); 
} 

编缉推荐阅读以下文章

  • 开放源码 C/C++ 单元测试工具,第 2 部分: 了解 CppUnit
  • 开放源码 C/C++ 单元测试工具,第 1 部分: 了解 Boost 单元测试框架
  • C++单元测试工具CppUnit使用简介
  • 单元测试工具C++ Test简介

  run 方法返回一个 Boolean 值,只有所有单元测试都成功,这个值才会设置为 True。run 方法的参数是一个 TextOutput 类型的对象。TextOutput 类处理测试日志的输出。在默认情况下,日志输出到屏幕。

  除了详细模式,还有简洁模式。这两种模式的差异是,详细模式输出测试中失败的断言的行号/文件⌒畔ⅲ而简洁模式只提供成功或失败的测试数量。

  失败后继续执行

  那么,如果一个测试失败了,会怎么样?是否继续执行测试由客户机代码决定。默认行为是继续执行其他测试。清单 5 给出 run 方法的原型。

清单 5. run 方法的原型

bool Test::Suite::run( 
  Output &  output, 
  bool     cont_after_fail = true  
); 

  如果在探测到第一个失败之后回归测试必须退出,那么 run 方法的第二个参数应该是 False。但是,可能不容易判断什么时候应该把第二个标志设置为 False。假设客户机代码试图向一个已经完全满了的磁盘写信息:代码将失败,套件中行为相似的所有其他测试也会失败。在这种情况下,立即停止回归测试是合理的。

  输出格式化器

  输出格式化器背后的思想是,可能需要不同格式的回归测试运行报告:文本、HTML 等等。因此,run 方法本身并不转储结果,而是接受一个 Output 类型的对象,它负责显示结果。在 CppTest 中可以使用三种输出格式化器:

  Test::TextOutput。这是所有输出处理器中最简单的一种。显示模式可以是详细或简洁。

  Test::CompilerOutput。按照与编译器构建日志相似的方式生成输出。

  Test::HtmlOutput。生成 HTML 输出。

  在默认情况下,这三种格式化器都把输出发送到 std::cout。前两种格式化器的构造器接受一个 std::ostream 类型的参数,它指定输出的目标,例如可以把输出转储到文件中以便进一步分析。还可以创建输出格式化器的定制版本。为此,只需从 Test::Output 派生出用户定义的格式化器。可以通过清单 6 中的代码理解不同的输出格式。

编缉推荐阅读以下文章

  • 开放源码 C/C++ 单元测试工具,第 2 部分: 了解 CppUnit
  • 开放源码 C/C++ 单元测试工具,第 1 部分: 了解 Boost 单元测试框架
  • C++单元测试工具CppUnit使用简介
  • 单元测试工具C++ Test简介

清单 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_FAIL 是 cppTest.h 中预定义的宏,它会导致断言失败。(稍后进一步讨论。)清单 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; 
} 

编缉推荐阅读以下文章

  • 开放源码 C/C++ 单元测试工具,第 2 部分: 了解 CppUnit
  • 开放源码 C/C++ 单元测试工具,第 1 部分: 了解 Boost 单元测试框架
  • C++单元测试工具CppUnit使用简介
  • 单元测试工具C++ Test简介

  注意,输出格式与 GNU Compiler Collection (GCC) 生成的编译日志相似,见清单 10。

清单 10. GCC 风格的编译器输出

/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. Virtual 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; 
} 

编缉推荐阅读以下文章

  • 开放源码 C/C++ 单元测试工具,第 2 部分: 了解 CppUnit
  • 开放源码 C/C++ 单元测试工具,第 1 部分: 了解 Boost 单元测试框架
  • C++单元测试工具CppUnit使用简介
  • 单元测试工具C++ Test简介

  清单 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( ) { … }; 
}; 

编缉推荐阅读以下文章

  • 开放源码 C/C++ 单元测试工具,第 2 部分: 了解 CppUnit
  • 开放源码 C/C++ 单元测试工具,第 1 部分: 了解 Boost 单元测试框架
  • C++单元测试工具CppUnit使用简介
  • 单元测试工具C++ Test简介

  注意,不需要显式地调用设置和退出方法。这些例程不一定要声明为虚拟的,除非打算以后扩展测试套件。这两个例程的返回类型必须是 void 而且不接受参数。

  CppTest 中的宏

  CppTest 提供几个有用的宏,可以在测试客户机源代码的实际方法中使用它们。这些宏在 cpptest-assert.h 中定义,cpptest.h 包含这个头文件。下面讨论一些宏和可能的用例。注意,除非另外声明,下面提供的输出都使用详细模式。

  TEST_FAIL (message)

  这个宏(见清单 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 (expression)

  这个宏与 C 断言库例程相似,但是 TEST_ASSERT 可以在调试和版本构建时起作用。如果表达式的结果是 False,就指出一个错误。清单 17 给出这个宏的内部实现。

清单 17. TEST_ASSERT 宏实现

#define TEST_ASSERT(expr) \ 
{  \ 
 if (!(expr)) \ 
 { \ 
 assertment(::Test::Source(__FILE__, __LINE__, #expr)); \ 
   if (!continue_after_failure()) return; \ 
 } \ 
} 

编缉推荐阅读以下文章

  • 开放源码 C/C++ 单元测试工具,第 2 部分: 了解 CppUnit
  • 开放源码 C/C++ 单元测试工具,第 1 部分: 了解 Boost 单元测试框架
  • C++单元测试工具CppUnit使用简介
  • 单元测试工具C++ Test简介

  TEST_ASSERT_MSG (expression, message)

  这个宏与 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 (expression1, expression2, delta)

  如果 expression1 和 expression2 的差超过了 delta,就抛出异常。这个宏在 expression1 和 expression2 是浮点数的情况下尤其有用;例如,根据实际采用的舍入方法不同,4.3 可能存储为 4.299999 或 4.300001,因此在进行比较时需要 delta。另一个示例是测试操作系统 I/O 的代码:打开文件花费的时间不可能每次都相同,但是必须在一定的范围内。

  TEST_ASSERT_DELTA_MSG (expression, message)

  这个宏与 TEST_ASSERT_DELTA 宏相似,只是在断言失败时还显示消息。

  TEST_THROWS (expression, exception)

  这个宏检验表达式并期望遇到某种异常。如果没有捕捉到异常,就触发断言。注意,对表达式的实际值并不进行任何测试;要测试的是异常。请考虑清单 19 中的代码。

编缉推荐阅读以下文章

  • 开放源码 C/C++ 单元测试工具,第 2 部分: 了解 CppUnit
  • 开放源码 C/C++ 单元测试工具,第 1 部分: 了解 Boost 单元测试框架
  • C++单元测试工具CppUnit使用简介
  • 单元测试工具C++ Test简介

清单 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 无条件地抛出 int 类型的异常,所以这个测试会顺利通过。

  TEST_THROWS_ANYTHING (expression)

  有时候,客户机例程根据具体情况抛出不同类型的异常。可以使用 TEST_THROWS_ANYTHING 宏处理这种情况,这个宏不需要指定期望的异常类型。只要在执行客户机代码之后捕捉到异常,就不会触发断言。

  TEST_THROWS_MSG (expression, exception, message)

  这个宏与 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 

编缉推荐阅读以下文章

  • 开放源码 C/C++ 单元测试工具,第 2 部分: 了解 CppUnit
  • 开放源码 C/C++ 单元测试工具,第 1 部分: 了解 Boost 单元测试框架
  • C++单元测试工具CppUnit使用简介
  • 单元测试工具C++ Test简介

  CppUnit 和 CppTest 的对比

  本系列的 第 2 部分 讨论了另一个流行的开放源码单元测试框架 CppUnit。CppTest 框架比 CppUnit 简单得多,但是效果也不错。下面简要地对比这两个强大的工具:

  创建单元测试和测试套件的简便性。CppUnit 和 CppTest 都创建类方法形式的单元测试,类本身都派生自工具提供的 Test 类。但是,CppTest 的语法略微简单些,测试的注册在类构造器内进行。对于 CppUnit,需要额外的宏 CPPUNIT_TEST_SUITE 和 CPPUNIT_TEST_SUITE_ENDS。

  运行测试。CppTest 直接调用测试套件上的 run 方法,而 CppUnit 使用单独的 TestRunner 类,通过调用这个类的 run 方法运行测试。

  扩展测试层次结构。对于 CppTest,随时可以从以前的测试套件派生新类,从而扩展测试套件。新的类定义一些新函数,这些函数成为新的单元测试。只需在新类类型的对象上调用 run 方法。与之相反,CppUnit 需要使用 CPPUNIT_TEST_SUB_SUITE 宏和类继承来实现相同的效果。

  生成格式化的输出。CppTest 和 CppUnit 都允许定制输出。但是,CppTest 有预定义的 HTML 输出格式化器,而 CppUnit 没有。但是,只有 CppUnit 支持 XML 格式化。它们都支持文本和编译器式格式。

  创建测试装备。要想使用测试装备,CppUnit 要求测试类派生自 CppUnit::TestFixture。您必须提供设置和退出例程的定义。对于 CppTest,只需提供设置和退出例程的定义。这个解决方案显然更好,因为这让客户机代码更简单。

  预定义的宏支持。CppTest 和 CppUnit 都提供一组用于断言、处理浮点等的宏。

  头文件。 CppTest 只要求包含一个头文件,而 CppUnit 客户机代码必须根据使用的特性包含多个头文件,比如 HelperMacros.h 和 TextTestRunner.h。

  结束语

  单元测试是当今软件开发过程的基本部分,CppTest 可以帮助 C/C++ 开发人员执行代码测试,从而降低维护难度。应该注意,本系列中讨论的三个工具(Boost 测试框架、CppUnit 和 CppTest)使用相同的基本概念,比如装备、用于断言的宏和输出格式化器。这些工具都是开放源码的,可以根据需要轻松地定制代码(例如 CppTest 的 XML 输出器)。您还等什么呢?开始试试吧!

编缉推荐阅读以下文章

  • 开放源码 C/C++ 单元测试工具,第 2 部分: 了解 CppUnit
  • 开放源码 C/C++ 单元测试工具,第 1 部分: 了解 Boost 单元测试框架
  • C++单元测试工具CppUnit使用简介
  • 单元测试工具C++ Test简介
其它资源
来源声明

版权与免责声明
1、本站所发布的文章仅供技术交流参考,本站不主张将其做为决策的依据,浏览者可自愿选择采信与否,本站不对因采信这些信息所产生的任何问题负责。
2、本站部分文章来源于网络,其版权为原权利人所有。由于来源之故,有的文章未能获得作者姓名,署“未知”或“佚名”。对于这些文章,有知悉作者姓名的请告知本站,以便及时署名。如果作者要求删除,我们将予以删除。除此之外本站不再承担其它责任。
3、本站部分文章来源于本站原创,本站拥有所有权利。
4、如对本站发布的信息有异议,请联系我们,经本站确认后,将在三个工作日内做出修改或删除处理。
请参阅权责声明