什么是单元测试?
复杂的 C/C++
代码中很可能有 bug,到代码编写完成之后再来测试就像大海捞针。比较谨慎的办法是,在编写各个代码段时,针对特定的区域(例如,一些包含大量计算的 C
函数或声明队列等数据结构的 C++
类),添加专门的小测试(单元测试),以在编写代码的同时进行测试。按这种方式构建的回归测试套件包含一套单元测试和一个测试驱动程序,这个程序运行测试并报告结果。
回页首
为特定的函数或类生成测试
对于文本编辑器这样复杂的代码,外部测试者无法生成针对特定例程的测试 — 测试者不太了解内部代码组织。Boost 的优势就在于白箱测试 :由开发人员编写测试,对类和函数进行语义检查。这个过程极其重要,因为代码以后的维护者可能会破坏原来的逻辑,这时单元测试就会失败。通过使用白箱测试,常常很容易找到出错的地方,不必使用调试器。
请考虑 清单 1 中的简单字符串类。这个类并不健壮,我们使用 Boost 来测试它。
清单 1. 简单的字符串类
#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 头文件中包含所需的所有定义。
清单 2. 字符串类的单元测试
#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 中代码的输出。
清单 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++
片段展示了这些工具类别之间的差异。
清单 4. 使用 Boost 测试工具的三个变体
#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 中代码的输出。
清单 5. 理解 BOOST_REQUIRE 和 BOOST_CHECK 之间的差异
[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 提供了一个示例。
清单 6. 使用 Boost 测试检查函数和类方法
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 提供了详细代码。
清单 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,看起来没什么问题 — 至少从表面看是这样。
清单 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)。
清单 9. BOOST_CHECK_CLOSE_FRACTION 宏的语法
BOOST_CHECK_CLOSE_FRACTION (left-value, right-value, tolerance-limit);
|
清单 9 中没有使用 BOOST_CHECK
,而是使用 BOOST_CHECK_CLOSE_FRACTION
并指定公差限制为 0.0001。清单 10 给出了代码现在的样子。
清单 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 给出了输出。
清单 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,在比较时就会出现错误。
清单 12. 错误:BOOST_CHECK_CLOSE_FRACTION 的左值和右值参数的类型不同
[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 给出了详细的代码。
清单 13. 使用 Boost 测试工具验证复杂的断言
#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。
清单 14. BOOST_CHECK_PREDICATE 宏
#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 中可以看到,使用这个选项生成的输出很详细,有助于进行快速调试。
清单 15. 使用一个文件中的多个测试套件
#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 中代码的输出:
清单 16. 用 –log_level 选项运行多个测试套件
[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
的例程。
清单 17. 创建用于运行回归测试的主测试套件
#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 源代码本身,解释了这个方法的工作方式。
清单 18. 理解 master_test_suite 方法
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 提供了一个简单的示例。
清单 19. 基本的 Boost 装备
#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 给出了输出。
清单 20. Boost 装备的输出
[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)。
清单 21. 头文件 unit_test_suite.hpp 中的 Boost 装备定义
#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 给出的测试套件中,三个测试中只有一个使用装备。
清单 22. 同时包含装备和非装备测试的 Boost 测试套件
#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 使用一个全局装备。
清单 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
的;对于销毁函数也是这样。
清单 24. 在回归测试中使用多个全局装备
#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 等开放源码回归测试框架。