一、CppTest简介
CppTest 是一个简单、轻便的 C++ 单元测试框架,它具有实用性与可扩展性,支持多种输出格式,添加新的测试用例极其方便。CppTest 最大的优点是容易理解、便于掌握和使用。
二、CppTest部署原理
CppTest 作为一个动态库被调用,要使用 CppTest 进行测试,首先需要部署 CppTest,编译 CppTest 动态库。
在 http://sourceforge.net/projects/cpptest/ 上找到 CppTest 的最新源码包,将其下载并解压,解压后将生成一个名为 cpptest-1.1.1 的目录,进入该目录,阅读该其中的 INSTALL 文件,它告诉我们详细的安装步骤:
1、执行配置脚本:./configure。如果报错,需要根据提示解决相应的错误方能继续。如果成功,最终将生成 Makefile 文件。
2、执行 make 命令编译源代码。
3、通常,我们可以执行 make check 命令来检查编译是否成功。
4、执行 sudo make install 命令,安装 CppTest 极其相关的动态库、头文件等。注意,由于该命令需要将相关文件拷贝到系统目录下,所以务必以管理员权限执行。
5、我们可以使用 make clean 来轻度清理生成文件,使用 make distclean 来清理包括 Makefile 在内的所有生成文件。
三、CppTest应用原理
1、CppTest 中的宏
CppTest 提供几个有用的宏,可以在测试客户机源代码的实际方法中使用它们。这些宏在 cpptest-assert.h 中定义,cpptest.h 包含这个头文件。下面讨论一些宏和可能的用例。
1)TEST_ADD (function)
这个宏用于注册需要测试的函数接口(如清单1),该 function 必须指定为无返回值无参数类型的函数指针。function 函数内部可以添加需要测试的代码或其他 CppTest 宏。
清单 1.使用 TEST_ADD 宏的客户机代码
#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)TEST_FAIL (message)
这个宏无条件地产生失败(如清单2)。可以使用这个宏的典型情况是处理客户机函数的结果。如果结果不符合任何期望的结果,那么抛出一个包含消息的异常。当遇到 TEST_FAIL 宏时,不再执行单元测试中的其他代码。
清单 2. 使用 TEST_FAIL 宏的客户机代码
void myTestSuite::unitTest1 ( ) { int result = usercode( ); switch (result) { case 0: // Do Something case 1: // Do Something … default: TEST_FAIL (“Invalid result\n”); } }
3)TEST_ASSERT (expression)
这个宏与 C 语言标准库中的断言函数相似,但是 TEST_ASSERT 可以在调试和版本构建时起作用。如果表达式的结果是 False,就指出一个错误。清单 3 给出这个宏的内部实现。
清单 3. TEST_ASSERT 宏实现
#define TEST_ASSERT(expr) \ { \ if (!(expr)) \ { \ assertment(::Test::Source(__FILE__, __LINE__, #expr)); \ if (!continue_after_failure()) return; \ } \ }
4)TEST_ASSERT_MSG (expression, message)
这个宏与 TEST_ASSERT 相似,只是在断言失败时在输出中显示消息而不是表达式。下面是包含和不包含消息的宏示例。清单 4 显示遇到这两个宏时的输出。
TEST_ASSERT (1 + 1 == 0); TEST_ASSERT_MSG (1 + 1 == 0, “Invalid expression”);
清单 4. TEST_ASSERT 和 TEST_ASSERT_MSG 宏的输出
Test: compare Suite: CompareTestSuite File: /home/trevor/linux/mytest.cpp Line: 91 Message: 1 + 1 == 0 Test: compare Suite: CompareTestSuite File: /home/trevor/linux/mytest.cpp Line: 92 Message: Invalid Expression
5)TEST_ASSERT_DELTA (expression1, expression2, delta)
如果 expression1 和 expression2 的差超过了 delta,就抛出异常。这个宏在 expression1 和 expression2 是浮点数的情况下尤其有用;例如,根据实际采用的舍入方法不同,4.3 可能存储为 4.299999 或 4.300001,因此在进行比较时需要 delta。另一个示例是测试操作系统 I/O 的代码:打开文件花费的时间不可能每次都相同,但是必须在一定的范围内。
6)TEST_ASSERT_DELTA_MSG (expression, message)
这个宏与 TEST_ASSERT_DELTA 宏相似,只是在断言失败时还显示消息。
7)TEST_THROWS (expression, exception)
这个宏检验表达式并期望遇到某种异常。如果没有捕捉到异常,就触发断言。注意,对表达式的实际值并不进行任何测试;要测试的是异常。请考虑清单 5 中的代码。
清单 5. 处理整数异常
class myTest1 : public Test::Suite { … void func1( ) { TEST_THROWS (userCode( ), int); } public: myTest1( ) { TEST_ADD(myTest1::func1); } }; float userCode( ) throws(int) { … throw 1; return 0.1; }
注意,userCode 例程的返回类型并不重要:它也可以是实型或整型。因为这里的 userCode 无条件地抛出 int 类型的异常,所以这个测试会顺利通过。
8)TEST_THROWS_MSG (expression, exception, message)
这个宏与 TEST_THROWS 相似,只是它输出消息而不是表达式。下面是包含和不包含消息的宏示例。清单 6 显示遇到这两个宏时的输出。
TEST_THROWS(userCode( ), int); TEST_THROWS_MSG(userCode( ), int, “No expected exception of type int”);
清单 6. TEST_THROWS 和 TEST_THROWS_MSG 宏的输出
Test: func1 Suite: myTest1 File: /home/trevor/linux/mytest.cpp Line: 24 Message: userCode() Test: func2 Suite: myTest1 File: /home/trevor/linux/mytest.cpp Line: 32 Message: No expected exception of type int
9)TEST_THROWS_ANYTHING (expression)
有时候,客户机例程根据具体情况抛出不同类型的异常。可以使用 TEST_THROWS_ANYTHING 宏处理这种情况,这个宏不需要指定期望的异常类型。只要在执行客户机代码之后捕捉到异常,就不会触发断言。
2、CppTest 的测试套件
CppTest 的测试套件是单元测试用于测试源代码的特定部分。形式最简单的测试套件包含一组测试其他 C/C++ 代码的 C/C++ 函数。CppTest 在 Test 名称空间中定义一个名为 Suite 的类,它提供基本的测试套件功能。用户定义的测试套件必须扩展这个功能,定义作为实际单元测试运行的函数。清单 7 定义一个名为 myTest 的类,其中有两个函数,分别测试一段源代码。TEST_ADD 是用于注册测试的宏。
清单 7. 扩展基本的 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); } };
3、扩展测试套件
我们可以很容易地扩展测试套件的功能,创建测试套件的层次结构。每个测试套件可能测试不同的代码部分,例如分别测试编译器的解析、代码生成和代码优化,创建层次结构有助于更好地管理。清单 8 演示如何创建这样的层次结构。
清单 8. 创建测试套件层次结构
#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 类。清单 8 给出它的原型(取自头文件 cpptest-suite.h)。
清单 8. Suite::add 方法声明
class Suite { public: … void add(std::auto_ptr<Suite> suite); ... } ;
四、CppTest应用场景
CppTest常应用于白盒测试,可以检测一段代码执行所消耗的时间,结合 CppTest 提供的一些宏的使用,可以跟踪指定变量是否得到预期的值,还可以跟踪程序是否出现异常。
五、CppTest应用举例
#include <cstdlib> #include <cstring> #include <iostream> #include "cpptest.h" using namespace std; // 测试无条件失败断言,即测试 TEST_FAIL 宏 // class FailTestSuite : public Test::Suite { public: FailTestSuite() { TEST_ADD(FailTestSuite::success) TEST_ADD(FailTestSuite::always_fail) } private: void success() {} void always_fail() { // 始终失败 // TEST_FAIL("unconditional fail"); } }; // 测试比较断言 // class CompareTestSuite : public Test::Suite { public: CompareTestSuite() { TEST_ADD(CompareTestSuite::success) TEST_ADD(CompareTestSuite::compare) TEST_ADD(CompareTestSuite::delta_compare) } private: void success() {} void compare() { // 表达式为真所以该断言成功 // TEST_ASSERT(1 + 1 == 2) // 表达式为假所以该断言失败 // TEST_ASSERT(0 == 1); } void delta_compare() { // 0.5 与 0.7 的差值小于 0.3,所以断言成功 // TEST_ASSERT_DELTA(0.5, 0.7, 0.3); // 0.5 与 0.7 的差值大于 0.1,所以断言失败 // TEST_ASSERT_DELTA(0.5, 0.7, 0.1); } }; // 测试抛出异常断言 // class ThrowTestSuite : public Test::Suite { public: ThrowTestSuite() { TEST_ADD(ThrowTestSuite::success) TEST_ADD(ThrowTestSuite::test_throw) } private: void success() {} void test_throw() { // 因为没有函数抛出异常,所以抛出异常断言将失败 // TEST_THROWS_MSG(func(), int, "func() does not throw, expected int exception") TEST_THROWS_MSG(func_no_throw(), int, "func_no_throw() does not throw, expected int exception") TEST_THROWS_ANYTHING_MSG(func(), "func() does not throw, expected any exception") TEST_THROWS_ANYTHING_MSG(func_no_throw(), "func_no_throw() does not throw, expected any exception") // 因为没有函数抛出异常,所以无抛出异常断言将成功 // TEST_THROWS_NOTHING(func()) TEST_THROWS_NOTHING(func_no_throw()) // 因为 func_throw_int() 抛出一个 int 异常,所以抛出断言将成功 // TEST_THROWS(func_throw_int(), int) TEST_THROWS_ANYTHING(func_throw_int()) // 因为 func_throw_int() 抛出一个 int (而不是 float) 异常,所以该抛出异常断言将失败,无抛出异常断言也将失败 // TEST_THROWS_MSG(func_throw_int(), float, "func_throw_int() throws an int, expected a float exception") TEST_THROWS_NOTHING_MSG(func_throw_int(), "func_throw_int() throws an int, expected no exception at all") } void func() {} void func_no_throw() throw() {} void func_throw_int() throw(int) { throw 13; } }; enum OutputType { Compiler, Html, TextTerse, TextVerbose }; static void usage() { cout << "usage: mytest [MODE]\n" << "where MODE may be one of:\n" << " --compiler\n" << " --html\n" << " --text-terse (default)\n" << " --text-verbose\n"; exit(0); } static auto_ptr<Test::Output> cmdline(int argc, char* argv[]) { if (argc > 2) usage(); // 内部直接退出程序,将不再返回 Test::Output* output = 0; if (argc == 1) output = new Test::TextOutput(Test::TextOutput::Verbose); else { const char* arg = argv[1]; if (strcmp(arg, "--compiler") == 0) output = new Test::CompilerOutput; else if (strcmp(arg, "--html") == 0) output = new Test::HtmlOutput; else if (strcmp(arg, "--text-terse") == 0) output = new Test::TextOutput(Test::TextOutput::Terse); else if (strcmp(arg, "--text-verbose") == 0) output = new Test::TextOutput(Test::TextOutput::Verbose); else { cout << "invalid commandline argument: " << arg << endl; usage(); // 内部直接退出程序,将不再返回 } } return auto_ptr<Test::Output>(output); } // 测试程序主函数 // int main(int argc, char* argv[]) { try { // 演示使用多个测试套件的功能 // Test::Suite ts; ts.add(auto_ptr<Test::Suite>(new FailTestSuite)); ts.add(auto_ptr<Test::Suite>(new CompareTestSuite)); ts.add(auto_ptr<Test::Suite>(new ThrowTestSuite)); // 运行测试 // auto_ptr<Test::Output> output(cmdline(argc, argv)); ts.run(*output, true); Test::HtmlOutput* const html = dynamic_cast<Test::HtmlOutput*>(output.get()); if (html) html->generate(cout, true, "MyTest"); } catch (...) { cout << "unexpected exception encountered\n"; return EXIT_FAILURE; } return EXIT_SUCCESS; }
六、CppTest测试报告
CppTest特有一个格式化输出器,它专门负责生成测试报告。
格式化输出器背后的思想是,可能需要不同格式的回归测试运行报告:文本、HTML 等等。因此,run 方法本身并不转储结果,而是接受一个 Output 类型的对象,它负责显示结果。在 CppTest 中可以使用三种输出格式化器:
Test::TextOutput。这是所有输出处理器中最简单的一种。显示模式可以是详细或简洁。 Test::CompilerOutput。按照与编译器构建日志相似的方式生成输出。 Test::HtmlOutput。生成 HTML 输出。
在默认情况下,这三种格式化器都把输出发送到 std::cout。前两种格式化器的构造器接受一个 std::ostream 类型的参数,它指定输出的目标,例如可以把输出转储到文件中以便进一步分析。还可以创建输出格式化器的定制版本。为此,只需从 Test::Output 派生出用户定义的格式化器。可以通过清单 9 – 12 中的代码理解不同的输出格式。
清单 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::TextOutput output(Test::TextOutput::Terse); return test1.run(output) ? 1 : 0; }
清单 10. 简洁输出只显示失败数
failTest1: 1/1, 0% correct in 0.000000 seconds Total: 1 tests, 0% correct in 0.000000 seconds
清单 11. 以详细模式运行 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::Verbose); return test1.run(output) ? 1 : 0; }
清单 12. 详细输出显示文件/行信息、消息、测试套件信息等等
failTest1: 1/1, 0% correct in 0.000000 seconds Test: always_fail Suite: failTest1 File: /home/trevor/linux/mytest.cpp Line: 5 Message: "This always fails!\n" Total: 1 tests, 0% correct in 0.000000 seconds
清单 13. 用编译器式输出格式运行 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; }
清单 14. GCC 风格的编译器输出
/home/trevor/linux/mytest.cpp:5: “This always fails!\n”
打印 HTML 风格的输出格式使用 HtmlOutput。注意,HTML 格式化器并不在构造器中接受文件句柄,而是依靠 generate 方法。generate 方法的第一个参数是一个 std::ostream 类型的对象,它的默认值是 std::cout。可以使用文件句柄把日志重定向到其他地方。清单 15 提供一个示例。
清单 15. 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; }
清单 16. 生成 HTML 格式的输出片段并存储到 test.log 文件中
… <table summary="Test Failure"> <tr> <td style="width:15%">Test</td> <td>failTest1::always_fail</td> </tr> <tr> <td style="width:15%">File</td> <td>/home/arpan/test/mytest.cpp:18</td> </tr> <tr> <td style="width:15%">Message</td> <td>"This always fails!\n"</td> </tr> </table> …
除非注明,文章均为CppLive 编程在线原创,转载请注明出处,谢谢。
我也转web咯,linux少不了,暑假好好玩一下。什么gcc,gdb,makefile etc..以前只是玩过一点,简单的应用规则,看来要好好深入一下了
看来还是学生哦,趁着暑假时间比较充沛,可以好好学习下^^
多多交流,实际开发一个项目应该会遇到很多的问题
好的,有问题欢迎交流^^