本文是探讨开放源码单元测试工具的系列的结尾一篇,细致引见 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);
}
}; 扩展测试套件
您能够很容易扩展测试套件的功用,创立测试套件的层次构造。每个测试套件能够测试不一样的代码局部,比如辨别测试编译器的分析、代码生成和代码优化,创立层次构造有助于更好地维护。清单 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 办法前往一个 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 中的代码明白不一样的输出格式。
清单 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;
} 留意,输出格式与 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;
} 清单 14 显示在 test.log 中生成的局部 HTML 输出。
清单 14. 生成的 HTML 输出的片段
…
<table summary="Test Failure" /> <tr>
<td style="width:15%" /> <td href="#/wangluoruanjian/liaotiangongju/3790.html" target="_blank" title="UC 下载" >UCcess">failTest1::always_fail</td>
</tr>
<tr>
<td style="width:15%" /> <td /> </tr>
<tr>
<td style="width:15%" /> <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( ) { … };
}; 留意,不须要显式地调用配置和退出办法。这些例程不必须要声明为虚拟的,除非方案现在扩展测试套件。这两个例程的前往类型必需是 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; \
} \
} 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 中的代码。
清单 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 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 输出器)。您还等什么呢?开端试试吧!
读库教程网文章由网络收集后整理发布,文章发布人拥有该内容的所有权力及责任!
如果你喜欢这页,可以按Ctrl+D收藏起来。







