什么是单元测试?
复杂的C/C++代码中很可能有 bug,到代码编写完成之后再来测试就像大海捞针。比较谨慎的办法是,在编写各个代码段时,针对特定的区域(例如,一些包含大量计算的C函数或声明队列等数据结构的C++类),添加专门的小测试(单元测试),以在编写代码的同时进行测试。按这种方式构建的回归测试套件包含一套单元测试和一个测试驱动程序,这个程序运行测试并报告结果。
回页首
为特定的函数或类生成测试
对于文本编辑器这样复杂的代码,外部测试者无法生成针对特定例程的测试 — 测试者不太了解内部代码组织。Boost 的优势就在于白箱测试 :由开发人员编写测试,对类和函数进行语义检查。这个过程极其重要,因为代码以后的维护者可能会破坏原来的逻辑,这时单元测试就会失败。通过使用白箱测试,常常很容易找到出错的地方,不必使用调试器。
请考虑 清单 1 中的简单字符串类。这个类并不健壮,我们使用 Boost 来测试它。
#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,访问范围超出索引是否导致错误消息或异常,等等。清单 2 给出了一些值得为任何字符串实现创建的测试。要想运行 清单 2 中的源代码,只需用 g++(或任何符合标准的C++编译器)编译它。注意,不需要单独的主函数,代码也不使用任何链接库:作为 Boost 一部分的 unit_test.hpp 头文件中包含所需的所有定义。
#define BOOST_TEST_MODULE stringtest #include <boost/test/included/unit_test.hpp> #include "./str.h" BOOST_AUTO_TEST_SUITE (stringtest) // name of the test suite is stringtest BOOST_AUTO_TEST_CASE (test1) { mystring s; BOOST_CHECK(s.size() == 0); } BOOST_AUTO_TEST_CASE (test2) { mystring s; s.setbuffer("hello world"); BOOST_REQUIRE_EQUAL ('h', s[0]); // basic test } BOOST_AUTO_TEST_SUITE_END( ) |
BOOST_AUTO_TEST_SUITE和BOOST_AUTO_TEST_SUITE_END宏分别表示测试套件的开头和结尾。各个测试放在这两个宏之间,从这一点来看,这些宏的语义很像C++名称空间。每个单元测试用BOOST_AUTO_TEST_CASE宏来定义。清单 3 给出了 清单 2 中代码的输出。
[arpan@tintin] ./a.out Running 2 test cases... test.cpp(10): error in "test1": check s.size() == 0 failed *** 1 failure detected in test suite "stringtest" |
下面详细讨论如何创建前面清单中的单元测试。基本思想是使用 Boost 提供的宏来测试各个类特性。BOOST_CHECK和BOOST_REQUIRE_EQUAL是 Boost 提供的预定义宏(也称为测试工具),用于验证代码输出。
回页首
Boost 测试工具
Boost 有一整套测试工具,基本上可以说它们是用于验证表达式的宏。测试工具的三个主要类别是BOOST_WARN、BOOST_CHECK和BOOST_REQUIRE。BOOST_CHECK和BOOST_REQUIRE之间的差异在于:对于前者,即使断言失败,测试仍然继续执行;而对于后者,认为这是严重的错误,测试会停止。清单 4 使用一个简单的C++片段展示了这些工具类别之间的差异。
#define BOOST_TEST_MODULE enumtest #include <boost/test/included/unit_test.hpp> BOOST_AUTO_TEST_SUITE (enum-test) BOOST_AUTO_TEST_CASE (test1) { typedef enum {red = 8, blue, green = 1, yellow, black } color; color c = green; BOOST_WARN(sizeof(green) > sizeof(char)); BOOST_CHECK(c == 2); BOOST_REQUIRE(yellow > red); BOOST_CHECK(black != 4); } BOOST_AUTO_TEST_SUITE_END( ) |
第一个BOOST_CHECK会失败,第一个BOOST_REQUIRE也是如此。但是,当BOOST_REQUIRE失败时,代码退出,所以不会到达第二个BOOST_CHECK。清单 5 显示了 清单 4 中代码的输出。
[arpan@tintin] ./a.out Running 1 test case... e2.cpp(11): error in "test1": check c == 2 failed e2.cpp(12): fatal error in "test1": critical check yellow > red failed *** 2 failures detected in test suite "enumtest" |
同样,如果需要针对特定情况检查某些函数或类方法,最容易的方法是创建一个新测试,并使用参数和期望值调用这个例程。清单 6 提供了一个示例。
BOOST_AUTO_TEST(functionTest1) { BOOST_REQUIRE(myfunc1(99, ‘A’, 6.2) == 12); myClass o1(“hello world!\n”); BOOST_REQUIRE(o1.memoryNeeded( ) < 16); } |
模式匹配
经常需要根据 “黄金日志” 测试函数生成的输出。BOOST_CHECK也适合执行这种测试,这还需要用到 Boost 库的output_test_stream类。用黄金日志文件(以下示例中的 run.log)初始化output_test_stream。C/C++函数的输出被提供给这个output_test_stream对象,然后调用这个对象的match_pattern例程。清单 7 提供了详细代码。
#define BOOST_TEST_MODULE example #include <boost/test/included/unit_test.hpp> #include <boost/test/output_test_stream.hpp> using boost::test_tools::output_test_stream; BOOST_AUTO_TEST_SUITE ( test ) BOOST_AUTO_TEST_CASE( test ) { output_test_stream output( "run.log", true ); output << predefined_user_func( ); BOOST_CHECK( output.match_pattern() ); } BOOST_AUTO_TEST_SUITE_END( ) |
浮点比较
回归测试中最棘手的检查之一是浮点比较。请看一下 清单 8,看起来没什么问题 — 至少从表面看是这样。
#define BOOST_TEST_MODULE floatingTest #include <boost/test/included/unit_test.hpp> #include <cmath> BOOST_AUTO_TEST_SUITE ( test ) BOOST_AUTO_TEST_CASE( test ) { float f1 = 567.0102; float result = sqrt(f1); // this could be my_sqrt; faster implementation // for some specific DSP like hardware BOOST_CHECK(f1 == result * result); } BOOST_AUTO_TEST_SUITE_END( ) |
运行这个测试时,尽管使用的是作为标准库一部分提供的sqrt函数,BOOST_CHECK宏仍然会失败。什么地方出错了?浮点比较的问题在于精度 —f1和result*result在小数点后面的几位不一致。为了解决这个问题,Boost 测试实用程序提供了BOOST_WARN_CLOSE_FRACTION、BOOST_CHECK_CLOSE_FRACTION和BOOST_REQUIRE_CLOSE_FRACTION宏。要想使用这三个宏,必须包含预定义的 Boost 头文件 floating_point_comparison.hpp。这三个宏的语法是相同的,所以本文只讨论 check 变体(见 清单 9)。
BOOST_CHECK_CLOSE_FRACTION (left-value, right-value, tolerance-limit); |
清单 9 中没有使用BOOST_CHECK,而是使用BOOST_CHECK_CLOSE_FRACTION并指定公差限制为 0.0001。清单 10 给出了代码现在的样子。
#define BOOST_TEST_MODULE floatingTest #include <boost/test/included/unit_test.hpp> #include <boost/test/floating_point_comparison.hpp> #include <cmath> BOOST_AUTO_TEST_SUITE ( test ) BOOST_AUTO_TEST_CASE( test ) { float f1 = 567.01012; float result = sqrt(f1); // this could be my_sqrt; faster implementation // for some specific DSP like hardware BOOST_CHECK_CLOSE_FRACTION (f1, result * result, 0.0001); } BOOST_AUTO_TEST_SUITE_END( ) |
这段代码运行正常。现在,把 清单 10 中的公差限制改为 0.0000001。清单 11 给出了输出。
[arpan@tintin] ./a.out Running 1 test case... sq.cpp(18): error in "test": difference between f1{567.010132} and result * result{567.010193} exceeds 1e-07 *** 1 failure detected in test suite "floatingTest" |
生产软件中另一个常见的问题是比较double和float类型的变量。BOOST_CHECK_CLOSE_FRACTION的优点是它不允许进行这种比较。这个宏中的左值和右值必须是相同类型的 — 即要么是float,要么是double。在 清单 12 中,如果f1是 double,而result是 float,在比较时就会出现错误。
[arpan@tintin] g++ sq.cpp -I/u/c/lib/boost /u/c/lib/boost/boost/test/test_tools.hpp: In function `bool boost::test_tools::tt_detail::check_frwd(Pred, const boost::unit_test::lazy_ostream&, boost::test_tools::const_string, size_t, boost::test_tools::tt_detail::tool_level, boost::test_tools::tt_detail::check_type, const Arg0&, const char*, const Arg1&, const char*, const Arg2&, const char*) [with Pred = boost::test_tools::check_is_close_t, Arg0 = double, Arg1 = float, Arg2 = boost::test_tools::fraction_tolerance_t<double>]': sq.cpp:18: instantiated from here /u/c/lib/boost/boost/test/test_tools.hpp:523: error: no match for call to `(boost::test_tools::check_is_close_t) (const double&, const float&, const boost::test_tools::fraction_tolerance_t<double>&)' |
定制的断言支持
Boost 测试工具验证 Boolean 条件。可以通过扩展测试工具支持更复杂的检查 — 例如,判断两个列表的内容是否相同,或者某一条件对于向量的所有元素是否都是有效的。还可以通过扩展BOOST_CHECK宏执行定制的断言检查。下面对用户定义的C函数生成的列表内容执行定制的检查:检查结果中的所有元素是否都大于 1。定制检查函数需要返回boost::test_tools::predicate_result类型。清单 13 给出了详细的代码。
#define BOOST_TEST_MODULE example #include <boost/test/included/unit_test.hpp> boost::test_tools::predicate_result validate_list(std::list<int>& L1) { std::list<int>::iterator it1 = L1.begin( ); for (; it1 != L1.end( ); ++it1) { if (*it1 <= 1) return false; } return true; } BOOST_AUTO_TEST_SUITE ( test ) BOOST_AUTO_TEST_CASE( test ) { std::list<int>& list1 = user_defined_func( ); BOOST_CHECK( validate_list(list1) ); } BOOST_AUTO_TEST_SUITE_END( ) |
predicate_result对象有一个隐式的构造函数,它接受一个 Boolean 值,因此即使validate_list的期望类型和实际返回类型不同,代码仍然会正常运行。
还有另一种用 Boost 测试复杂断言的方法:BOOST_CHECK_PREDICATE宏。这个宏的优点是它不使用predicate_result。但缺点是语法有点儿粗糙。用户需要向BOOST_CHECK_PREDICATE宏传递函数名和参数。清单 14 的功能与 清单 13 相同,但是使用的宏不同。注意,validate_result的返回类型现在是 Boolean。
#define BOOST_TEST_MODULE example #include <boost/test/included/unit_test.hpp> bool validate_list(std::list<int>& L1) { std::list<int>::iterator it1 = L1.begin( ); for (; it1 != L1.end( ); ++it1) { if (*it1 <= 1) return false; } return true; } BOOST_AUTO_TEST_SUITE ( test ) BOOST_AUTO_TEST_CASE( test ) { std::list<int>& list1 = user_defined_func( ); BOOST_CHECK_PREDICATE( validate_list, list1 ); } BOOST_AUTO_TEST_SUITE_END( ) |
回页首
在一个文件中包含多个测试套件
可以在一个文件中包含多个测试套件。文件中定义的每个测试套件必须有一对BOOST_AUTO_TEST_SUITE... BOOST_AUTO_TEST_SUITE_END宏。清单 15 给出了在同一个文件中定义的两个测试套件。在运行回归测试时,用预定义的–log_level=test_suite选项运行可执行程序。在 清单 16 中可以看到,使用这个选项生成的输出很详细,有助于进行快速调试。
#define BOOST_TEST_MODULE Regression #include <boost/test/included/unit_test.hpp> typedef struct { int c; char d; double e; bool f; } Node; typedef union { int c; char d; double e; bool f; } Node2; BOOST_AUTO_TEST_SUITE(Structure) BOOST_AUTO_TEST_CASE(Test1) { Node n; BOOST_CHECK(sizeof(n) < 12); } BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(Union) BOOST_AUTO_TEST_CASE(Test1) { Node2 n; BOOST_CHECK(sizeof(n) == sizeof(double)); } BOOST_AUTO_TEST_SUITE_END() |
下面是 清单 15 中代码的输出:
[arpan@tintin] ./a.out --log_level=test_suite Running 2 test cases... Entering test suite "Regression" Entering test suite "Structure" Entering test case "Test1" m2.cpp(23): error in "Test1": check sizeof(n) < 12 failed Leaving test case "Test1" Leaving test suite "Structure" Entering test suite "Union" Entering test case "Test1" Leaving test case "Test1" Leaving test suite "Union" Leaving test suite "Regression" *** 1 failure detected in test suite "Regression" |
回页首
理解测试套件的组织
到目前为止,本文已经讨论了 Boost 测试实用程序和没有层次结构的测试套件。现在,我们使用 Boost 创建一个测试套件,以外部工具用户常见的方式测试软件产品。在测试框架中,通常有多个套件,每个套件检查产品的某些特性。例如,文字处理程序的回归测试框 架应该包含检查字体支持、不同的文件格式等方面的套件。每个测试套件包含多个单元测试。清单 17 提供了一个测试框架示例。注意,代码入口点必须是名为init_unit_test_suite的例程。
#define BOOST_TEST_MODULE MasterTestSuite #include <boost/test/included/unit_test.hpp> using boost::unit_test; test_suite* init_unit_test_suite( int argc, char* argv[] ) { test_suite* ts1 = BOOST_TEST_SUITE( "test_suite1" ); ts1->add( BOOST_TEST_CASE( &test_case1 ) ); ts1->add( BOOST_TEST_CASE( &test_case2 ) ); test_suite* ts2 = BOOST_TEST_SUITE( "test_suite2" ); ts2->add( BOOST_TEST_CASE( &test_case3 ) ); ts2->add( BOOST_TEST_CASE( &test_case4 ) ); framework::master_test_suite().add( ts1 ); framework::master_test_suite().add( ts2 ); return 0; } |
每个测试套件(比如 清单 17 中的ts1)都是使用BOOST_TEST_SUITE宏创建的。这个宏需要一个字符串作为测试套件的名称。最终使用add方法,把所有测试套件添加到主测试套件中。同样,我们使用BOOST_TEST_CASE宏创建每个测试,然后再使用add方法把它们添加到测试套件中。也可以把单元测试添加到主测试套件中,但是不建议这么做。master_test_suite方法属于boost::unit_test::framework名称空间的一部分:它在内部实现一个单实例对象。清单 18 中的代码取自 Boost 源代码本身,解释了这个方法的工作方式。
master_test_suite_t& master_test_suite() { if( !s_frk_impl().m_master_test_suite ) s_frk_impl().m_master_test_suite = new master_test_suite_t; return *s_frk_impl().m_master_test_suite; } |
使用BOOST_TEST_CASE宏创建的单元测试以函数指针作为输入参数。在 清单 17 中,test_case1、test_case2等是 void 函数,用户可以按自己喜欢的方式编写代码。但是注意,Boost 测试设置会使用一些堆内存;每个对BOOST_TEST_SUITE的调用都会产生一个新的boost::unit_test::test_suite(<test suite name>)。
回页首
装备
从概念上讲,测试装备(test fixture)是指在执行测试之前设置一个环境,在测试完成时清除它。清单 19 提供了一个简单的示例。
#define BOOST_TEST_MODULE example #include <boost/test/included/unit_test.hpp> #include <iostream> struct F { F() : i( 0 ) { std::cout << "setup" << std::endl; } ~F() { std::cout << "teardown" << std::endl; } int i; }; BOOST_AUTO_TEST_SUITE( test ) BOOST_FIXTURE_TEST_CASE( test_case1, F ) { BOOST_CHECK( i == 1 ); ++i; } BOOST_AUTO_TEST_SUITE_END() |
清单 20 给出了输出。
[arpan@tintin] ./a.out Running 1 test case... setup fix.cpp(16): error in "test_case1": check i == 1 failed teardown *** 1 failure detected in test suite "example" |
这段代码没有使用BOOST_AUTO_TEST_CASE宏,而是使用BOOST_FIXTURE_TEST_CASE,它需要另一个输入参数。这个对象的constructor和destructor方法执行必需的设置和清除工作。看一下 Boost 头文件 unit_test_suite.hpp 就可以确认这一点(见 清单 21)。
#define BOOST_FIXTURE_TEST_CASE( test_name, F ) \ struct test_name : public F { void test_method(); }; \ \ static void BOOST_AUTO_TC_INVOKER( test_name )() \ { \ test_name t; \ t.test_method(); \ } \ \ struct BOOST_AUTO_TC_UNIQUE_ID( test_name ) {}; \ \ BOOST_AUTO_TU_REGISTRAR( test_name )( \ boost::unit_test::make_test_case( \ &BOOST_AUTO_TC_INVOKER( test_name ), #test_name ), \ boost::unit_test::ut_detail::auto_tc_exp_fail< \ BOOST_AUTO_TC_UNIQUE_ID( test_name )>::instance()->value() ); \ \ void test_name::test_method() \ |
在内部,Boost 从struct F公共地派生一个类(见 清单 19),然后从这个类创建对象。按照C++的公共继承规则,在函数中可以直接访问struct类的所有受保护变量和公共变量。注意,在 清单 19 中修改的变量i属于类型为F的内部对象t(见 清单 20)。在回归测试套件中可能只有几个测试需要某种显式的初始化,因此可以只对它们使用装备特性。在 清单 22 给出的测试套件中,三个测试中只有一个使用装备。
#define BOOST_TEST_MODULE example #include <boost/test/included/unit_test.hpp> #include <iostream> struct F { F() : i( 0 ) { std::cout << "setup" << std::endl; } ~F() { std::cout << "teardown" << std::endl; } int i; }; BOOST_AUTO_TEST_SUITE( test ) BOOST_FIXTURE_TEST_CASE( test_case1, F ) { BOOST_CHECK( i == 1 ); ++i; } BOOST_AUTO_TEST_CASE( test_case2 ) { BOOST_REQUIRE( 2 > 1 ); } BOOST_AUTO_TEST_CASE( test_case3 ) { int i = 1; BOOST_CHECK_EQUAL( i, 1 ); ++i; } BOOST_AUTO_TEST_SUITE_END() |
在 清单 22 中,在一个测试用例上定义和使用了装备。Boost 还允许用户通过BOOST_GLOBAL_FIXTURE (<Fixture Name>)宏定义和使用全局装备。可以定义任意数量的全局装备,因此可以把初始化代码分割为多个部分。清单 23 使用一个全局装备。
#define BOOST_TEST_MODULE example #include <boost/test/included/unit_test.hpp> #include <iostream> struct F { F() { std::cout << "setup" << std::endl; } ~F() { std::cout << "teardown" << std::endl; } }; BOOST_AUTO_TEST_SUITE( test ) BOOST_GLOBAL_FIXTURE( F ); BOOST_AUTO_TEST_CASE( test_case1 ) { BOOST_CHECK( true ); } BOOST_AUTO_TEST_SUITE_END() |
对于多个装备,它们的设置和清除按照声明的次序执行。在 清单 24 中,先调用F的构造函数,然后是F2的;对于销毁函数也是这样。
#define BOOST_TEST_MODULE example #include <boost/test/included/unit_test.hpp> #include <iostream> struct F { F() { std::cout << "setup" << std::endl; } ~F() { std::cout << "teardown" << std::endl; } }; struct F2 { F2() { std::cout << "setup 2" << std::endl; } ~F2() { std::cout << "teardown 2" << std::endl; } }; BOOST_AUTO_TEST_SUITE( test ) BOOST_GLOBAL_FIXTURE( F ); BOOST_GLOBAL_FIXTURE( F2 ); BOOST_AUTO_TEST_CASE( test_case1 ) { BOOST_CHECK( true ); } BOOST_AUTO_TEST_SUITE_END() |
注意,不能将全局装备作为对象用在单个测试中。也不能在测试中直接访问它们的公共/受保护的非静态方法或变量。
回页首
结束语
本文介绍了最强大的开放源码回归测试框架之一:Boost。讨论了基本的 Boost 检查、模式匹配、浮点比较、定制检查、测试套件的组织(包括手工和自动)和装备。请通过 Boost 文档了解更多信息。本系列的后续文章将介绍 cppUnit 等开放源码回归测试框架。
参考资料
学习
获得产品和技术