第6章正确性与测试
测试对于软件开发是非常重要的,程序员——尤其是C++程序员更应该认识到这一点。
但C/C++只提供了很有限的正确性验证/测试支持——assert宏 (没错,它是一个宏,虽然它违背常识使用了小写的形式),这是很不够的。C++98标准中的std::exception能够处理运行时异常、但并不能检查代码的逻辑,故C/C++都缺乏足够的、语言级别的工具来保证软件的正确性,使程序员很容易陷入与bug搏斗的泥沼中。
boost在这方面前进了一大步:boost.assert库增强了原始的运行时assert宏, static_assert库提供了静态断言(编译期诊断),而boost.test库则构建了完整的单元测试框架。
6.1 assert
boost.assert提供的主要工具是BOOST_ASSERT宏,它类似于C标准中的assert宏,提供运行时的断言,但功能有所增强。
为使用boost.assert需要包含头文件<boost/assert.hpp>,即
#include <boost/assert.hpp>
6.1.1 基本用法
默认情况下BOOST_ASSERT宏等同于assert宏,断言表达式为真,即:
#define BOOST_ASSERT(expr) assert (expr)
宏的参数expr表达式可以是任意(合法的)C + +表达式,从简单的关系比较到复杂的函数嵌套调用都允许。如果表达式值为true,那么断言成立,程序会继续向下执行,否则断言会引发一个异常,在终端上输出调试信息并终止程序的执行。例如:
BOOST_ASSERT(16 == 0x10) ; //断言成立
BOOST_ASSERT(string() .size() == 1) ; //断言失败,抛出异常
BOOST_ASSERT宏仅会在debug模式下生效,在release模式下不会进行编译,不会影响到运行效率,所以可以放心大胆地在代码中使用BOOST_ASSERT断言。它不仅能够诊断错误、保证程序正确运行,而且其规范的大写形式也能够起到类似代码注释的作用,提醒代码维护人员什么该做、什么不该做。
下面的例子演示了BOOST_ASSERT宏的一般用法,程序定义了一个取倒数函数£unc,使用BOOST_ASSERT 确保不会出现除以0的错误:
#include <boost/assert.hpp>
double func(int x)
{
BOOST_ASSERT (x ! = 0 && "divided by zero");
return 1.0 / x;
}
请读者注意:代码中BOOST_ASSERT宏表达式的用法,除了要检查的表达式“x != 0”,宏还使用逻辑与操作符&&向表达式增加了断言的描述信息。当断言失败时,可以给出更具描述性的文字,有助于排错。
在debug模式下以参数0调用函数func ()
int main()
{
func(0); //error
}
会导致程序异常终止报出如下的错误消息:
Assertion failed: x != 0 && "divided by zero", file d:\vc\main.cpp, line 48
6.1.2 禁用断言
BOOST_ASSERT是标准断言宏assert的增强版,因此它有更多的使用灵活性。
如果在头文件<boost/assert.hpp>之前定义了宏BOOST_DISABLE_ASSERTS,那么 BOOST_ASSERT将会定义为((void) 0),自动失效。但标准的assert宏并不会受影响,这可以让程序员有选择地关闭BOOST ASSERT 。
修改一下之前的例子
#define BOOST_DISABLE_ASSERTS
#include <cassert>
#include <boost/assert.hpp>
double func(int x)
{
BOOST_ASSERT(x != 0 && "divided by zero");//失效
cout << "after BOOST_ASSERT" << endl;
assert(x != 0 && "divided by zero"); //有效
cout << "after" << endl;
return 1.0 / x;
}
仍然调用£unc(0),程序运行结果将会是如下的样子
after BOOST_ASSERT
Assertion failed: x != 0 && "divided by zero", file d:\vc\ main.cpp, line 52
6.1.3 扩展用法
如果在<boost/assert.hpp>之前定义了BOOST_ENABLE_ASSERT_HANDLER, 这将导致BOOST_ASSERT的行为发生改变:
它将不再等同于assert宏,断言的表达式无论是在debug还是release模式下都将被求值。如果断言失败,会发生一个断言失败的函数调用boost::assertion_failed()——这相当于提供了一个错误处理handle。
函数assertion_failed()声明在boost名字空间里,但特意被设计为没有具体实现,其声明如下:
namespace boost {
void assertion_failed(char const * expr, char const * function, char const * file, long line);
当断言失败时,BOOST_ASSERT宏会把断言表达式字符串、调用函数名(使用BOOST_ CURRENT_FUNCTION,参见4 .12.2小节)、所在源文件名和行号都传递给assertion_failed()函数处理。用户需要自己实现assertion_failed()函数,以恰当的方式处理错误——通常是记录日志或者抛出异常。
演示assertion_failed()函数基本用法的代码如下,其中用到了 5.2小节的format库来代替C语言中的printf ()函数:
#include <boost/format.hpp>
namespace boost {
void assertion_failed(char const * expr, char const * function, char const * file, long line)
{
boost::format fmt("Assertion failed!\n Expression: %s\n\
Function: %s\nFile: %s\nLine: %ld\n\n");
fmt % expr % function % file % line;
cout << fmt;
}
} //namespace boost
#define BOOST_ENABLE_ASSERT_HANDLER
#include <boost/assert.hpp>
double func(int x){...}
int main(>
{
func{0); //error
}
程序的运行结果如下:
Assertion failed!
Expression: x != 0 && "divided by zero"
Function: double cdecl func(int)
File: d:\vc\main.cpp Line: 61
BOOST_ASSERT的这种错误handle的用法很有用,适合那些需要有统一错误处理方式的地方,最常见的就是函数入口参数检查:在函数入口断言参数,当参数出错时拼错误字符串,抛出参数异常类,同时终止函数的流程。
如果担心BOOST_ASSERT提供的详细诊断信息有泄漏源代码的危险,可以有选择地输出错误消息,或者对字符串加密。抛出异常的assertion_failed函数的实现可以是这样的:
void assertion_failed(...)
{
string str;
... //拼字符串
throw std::invalid_argument(str);
}
6.1.4 BOOST_VERIFY
BOOST_VERIFY宏是assert库提供的另一个工具。它具有与BOOST_ASSERT—样的行为,之前对BOOST_ASSERT的讨论对BOOST_VERIFY也同样适用。仅有一点区别:断言的表达式一定会被求值。在运用断言运算(而不仅仅是错误检查)及验证函数返回值时很有用。
示范BOOST_VERIFY宏的求值用法的代码如下:
#include <boost/assert.hpp>
...
int len;
BOOST_VERIFY(len = strlen("123")); //len 被求值
使用BOOST_VERIFY需要注意的是,它在release模式下同样会失效,程序最好不应该依赖于它的副作用。
6.2 static_assert
assert宏和BOOST_ASSERT宏是用于运行时的断言,它们是程序员的好帮手,应该在程序中被广泛应用。但有的时候,运行时的断言已经太晚了,程序已经发生了无可挽回的错误。
static_assert库把断言的诊断时刻由运行期提前到编译期,让编译器检查可能发生的错误,能够更好地增加程序的健壮性。
为了使用static_assert组件,需要包含头文件<boost/static_assert.hpp>,即:
#include < boost/static_assert.hpp>
6.2.1 用法
与BOOST_ASSERT类似,static_assert 库定义了宏 BOOST_STATIC_ASSERT,用来进行编译期断言,使用方法也比较相似。例如:
BOOST_STATIC_ASSERT(2 == sizeof(short));
BOOST_STATIC_ASSERT(true);
BOOST_STATIC_ASSERT(16 == 0x10);
BOOST_STATIC_ASSERT使用了较复杂的技术,但简单来理解,它实际上最终是一个 typedef,因此在编译时同样不会产生任何代码和数据,对运行效率不会有任何影响——不论是debug模式还是release模式。
BOOST_STATIC_ASSERT是一个编译期断言,使用了typedef和模板元技术实现,虽然在很多方面它都与BOOST_ASSERT很相似,但用法还是有所不同的。最重要的区别是使用范围, BOOST_ASSERT(assert)必须是一个能够执行的语句,它只能在函数域里出现,而 BOOST_STATIC_ASSERT则可以出现在程序的任何位置:名字空间域、类域或函数域。
下面的代码定义了一个简单的模板函数my_min,出于某种目的,它仅支持short或者char的类型:
#include <boost/static_assert.hpp>
template<typename T>
T my_min(T a, T b)
{
BOOST_STATIC_ASSERT (sizeof (T) < sizeof (int) ) ; //静态断言
return a < b? a: b;
}
int main()
{
cout << my_min((short)1, (short)3)
cout << my_min(1L, 3L);
}
静态断言的具体错误信息依据编译器而不同,在VC8下,main()的第二条语句会产生如下的提示:
error C2027: use of undefined type 'boost::STATIC_ASSERTION_FAILURE<x>*
断言的错误信息可能不够明显(“未定义的类型错误”,这是BOOST_STATIC_ASSERT的实现方式),但它能够指明错误的位置,并为解决错误指出了基本的方向。
BOOST_STATIC_ASSERT在类域和名字空间域的使用方式与在函数域的方式相同,例如:
namespace my_space {
class empty_class //一个“空”类
{
//在类域中静态断言,要求int至少4字节
BOOST_STATIC_ASSERT(sizeof(int)>=4);
};
//名字空间域静态断言,是一个“空类”
BOOST_STATIC_ASSERT(sizeof(empty_class) == 1);
}
这段代码同时展示了一个“有趣”的事实,“空类”其实并不空,因为C++不允许大小为0的类或对象的存在,通常的“空类”会由编译器在里面安插一个类型为char的成员变量,令它有一个确定的大小。
6.2.2使用建议
BOOST STATIC ASSERT主要在泛型编程或模板元编程中用于验证编译期常数或者模板类型参数,可能读者暂时不会用到它,但随着对C++认识的深入,也许不远的将来你就会发现它的好处。
使用BOOST_STATIC_ASSERT时还需要小心,断言的表达式必须能够在编译期求值,可能会让很多人不太适应,很正常,因为这正是迈向C++泛型编程和模板元编程的第一步。
在2 . 3节progress_timer有另一个static_assert的具体使用例子,读者可参考。
6.3 test
test库提供了一个用于单元测试的基于命令行界面的测试套件Unit Test Framework, 简称UTF(这可能会令有的读者联想到Unicode甚至XML,抱歉,在阅读本章时请完全忘记它们),还附带有检测内存泄漏的功能,比其他的单元测试库更强大更方便好用。它不仅能够支持简单的测试,也能够支持全面的单元测试,并且还具有程序运行监控功能,是一个用于保证程序正确性的强大工具。
为了使用test库需要包含头文件<boost/test/unit_test.hpp>,即:
#include <boost/test/unit_test.hpp>
6.3.1 编译test库
test库需要编译才能使用,bjam命令如下:
bjam -toolset=msvc -with-test -build-type=complete stdlib=stlport stage
如果不想编译test库,同样可以使用工程中嵌入实现代码的方式,从而享受与操作系统、编译器、Boost库版本无关的好处。
test库非常体贴地提供了预编译源码文件,不需要如date_time库那样自己动手实现。头文件<boost/test/included/unit_test.hpp>包含了test库的所有实现代码,因此我们需要在工程中加入cpp文件如下:
//test_main.cpp
#define BOOST_TEST_MAIN //定义主测试套件,是测试main函数入口
#include <boost/test/included/unit_test.hpp>
这样,将导致把test的所有实现代码编译进测试程序。其他测试套件的源代码文件仍然需要包含<boost/test/unit_test.hpp>,但前面应加入宏定义BOOST_TEST_INCLUDED,告诉test库我们使用嵌入源码的使用方式,即:
//test suitel.cpp
#define BOOST_TEST_INCLUDED
#include <boost/test/unit_test.hpp>
这样就相当于在文件test_main.cpp中编译了 test的静态库,而其他文件则仅包含test 库的声明,直接使用编译好的静态库。
6.3.2最小化的测试套件
test库提供一个最小化的测试套件minimal test,不需要对test库做任何形式的编译,只需要包含头文件<boost/test/ minimal.hpp>就可以使用,即:
#include <boost/test/minimal.hpp>
它只提供最基本的单元测试功能,没有UTF那么强大,不支持多个测试用例,能够使用的测试断言也很少。但因为功能简单小巧,适合入门和简单的测试。
头文件<boost/test/minimal.hpp>中已经实现了一个main(),因此我们不必再定义自己的main (),只需要实现一个test_main()函数,它是minimal test的真正功能函数。
test_main ()函数的声明与标准的main ()很相似:
int test_main( int argc, char* argv[])
在test_main()的函数体内,我们可以使用四个测试断言宏:
■ BOOST_CHECK(predicate) :断言测试通过,如不通过不影响程序执行;
■ BOOST_REQUIRE(predicate) :要求测试必须通过,否则程序停止执行;
■ BOOST_ERROR(message) :给出一个错误信息,程序继续执行;
■ BOOST_FAIL(message) :给出一个错误信息,程序运行终止。
示范minimal test用法的代码如下,我们用它来简单测试一下format库:
#include <boost/test/minimal.hpp> //最小化测试套件头文件
#include <boost/format.hpp>
#include <iostream>
int test_main( int argc, char* argv[] ) { //测试主函数
using namespace boost;
format fmt ("%d-%d") ;
BOOST_CHECK(fmt.size() != 0); //断言format对象已经初始化
fmt % 12 % 34;
BOOST_REQUIRE (fmt. str () == "12-34") ; //验证格式化结果
BOOST_ERROR("演示一条错误消息") ; //不影响程序的执行
fmt.clear();
fmt % 12;
try {
std: : cout << fmt; //输入参数不完整,抛出异常
}
catch (...)
{
BOOST_FAIL("致命错误,测试终止");
}
return 0;
}
这段代码虽然很短,但具备了单元测试的各个基本要素,说明了单元测试的基本步骤。
我们用test_main()函数建立了这个程序中唯一的一个测试用例、同时也是唯一的一个测试套件,测试的对象是boost::format。然后我们可以使用各种方法操作测试对象,并用类似 BOOST_ASSERT的测试断言宏来验证操作结果。如果操作结果如预期,那么一切都好;否则,单元测试框架会记录下断言失败的位置和数量。当遇到致命的错误,可能导致无法继续运行测试时,我们可以使用BOOST_FAIL来终止测试的运行。
单元测试程序运行结束后,minimal test会在控制台给出一个本次测试的总结,列出所有的失败错误断言和错误总数,可以据此跟踪错误发源地,进而纠正错误。
程序的运行结果大致是这样:
xxx. cpp (15):演示一条错误消息 in function: ' int cdecl test_main (int, char *[])'
xxx. cpp (25):致命错误,测试终止 in function: ' int cdecl test_main (int, char * [])'
**** 2 errors detected
minimal test简单好用,但它的功能非常有限,无法把它应用于大中型软件项目,因为那经常需要很多的测试用例和测试套件,而minimal test不能组织起复杂的测试结构。
minimal test仅适用于单元测试的演示,或者规模较小的程序,通常这样的程序是由一个人在几天之内完成的,要测试的接口不超过10个。
6.3.3 单元测试框架简介
test库提供了强有力的单元测试框架(UTF),它为软件开发的基本领域——单元测试提供了简单而富有弹性的解决方案,可以满足开发人员从高到低的各种需求,它的优点包括:
■易于理解,任何人都可以很容易地构建单元测试模块;
■提供测试用例、测试套件的概念,并能够以任意的复杂度组织它们;
■提供丰富的测试断言,能够处理各种情况,包括C++异常;
■可以很容易地初始化测试用例、测试套件或者整个测试程序;
■ 可以显示测试进度,这对于大型测试是非常有用的;
■ 测试信息可以显示为多种格式,如平文件或者XML格式;
■ 支持命令行,可以指定运行任意一个测试套件或测试用例;
■ 还有许多更高级的用法。
接下来我们将详细介绍UTF的各个组成部分,首先是测试断言,它是单元测试的基本工具。
6.3.4 测试断言
在test库中,测试断言是一组命名清楚的宏,它们的用法类似BOOST_ASSERT,断言测试通过。如果测试失败,则会记录出错的文件名和行号以及错误信息。
test库中一个典型的测试断言是BOOST_CHECK_EQUAL,形式是BOOST_XXX_YYY,具体命名规则如下:
■ BOOST_:遵循Boost库的命名规则,宏一律以大写的BOOST开头;
■ XXX:断言的级别。WARN是警告级,不影响程序运行,也不增加错误数量;CHECK是检査级别,如果断言失败增加错误数量,但不影响程序运行;REQUIRE是最高的级别,如果断言失败将增加错误数量并终止程序运行。最常用的断言级别是CHECK,WARN可以用于不涉及程序关键功能的测试,只有当断言失败会导致无法继续进行测试时才能够使用 REQUIRE;
■ YYY:各种具体的测试断言,如断言相等/不等、抛出/不抛出异常、大于或小于等等。
在6.3.2小节我们已经见到了四个基本的测试断言:BOOST_CHECK、BOOST_REQUIRE、BOOST_ERROR和BOOST_FAIL。它们是最基本的测试断言,能够在任何地方使用,但同时为了通用性也不具有其他断言的好处,我们应当尽量少使用它们。
test库中最常用的几个测试断言如下:
■ BOOST_XXX_EQUAL(l,r):检查1==r,当测试失败时会给出详细的信息。它不能用于浮点数的比较,浮点数的相等比较应使用BOOST_XXX_CLOSE;
■ BOOST_XXX_GE(l,r):检查l>=r,同样的还有GT(l>r)、LT (l<r)>、LE (l<=r) 和NE (l!=r),它们用于测试各种不等性;
■ BOOST XXX THROW (expr, exception):检测表达式expr抛出指定的exception异常;
■ BOOST_XXX_NO_THROW(expr, exception):检测表达式expr不抛出指定的exception异常;
■ BOOST_XXX_MESSAGE(expr, message):它与不带MESSAGE后缀的断言功能相同,但测试失败时给出指定的消息;
■ BOOST_TEST_MESSAGE (message):它仅输出通知用的信息,不含有任何警告或者错误,默认情况不会显示。
之后的几个小节将会看到这些测试断言的用法。
6.3.5 测试用例与套件
单元测试领域有很多专有概念,本小节仅介绍test库中最重要的几个。
test库将测试程序定义为一个测试模块,由测试安装、测试主体、测试清理和测试运行器四个部分组成。测试主体是测试模块的实际运行部分,由测试用例和测试套件组织成测试树的形式。
测试用例是一个包含多个测试断言的函数,它是可以被独立执行测试的最小单元,各个测试用例之间是无关的,发生的错误不会影响到其他测试用例。
要添加测试用例,需要向UTF注册。在test库中,可以采用手工或者自动两种形式,通常自动的方式更加简单易用,可以简化测试代码的编写。我们使用宏BOOST_AUTO_TEST_CASE像声明函数一样创建测试用例,它的定义是:
#define BOOST_AUTO_TEST_CASE( test_name )
宏的参数test_name是测试用例的名字,一般以t开头,表明整个名字是一个测试用例,例如:
BOOST_AUTO_TEST_CASE (t_casel) //测试用例声明
{
BOOST_CHECK_EQUAL (1, 1); //测试1==1
...
//其他测试断言
}
测试套件是测试用例的容器,它包含一个或多个测试用例,可以将繁多的测试用例分组管理,共享安装/清理代码,更好地组织测试用例。测试套件可以嵌套,并且没有嵌套层数的限制。
测试套件同样可以有手工和自动两种使用方式,自动方式使用两个宏BOOST_AUTO_TEST_ SUITE 和 BOOST_AUTO_TEST_SUITE_END,它们的定义是:
#define BOOST_AUTO_TEST_SUITE( suite_name )
#define BOOST_AUTO_TEST_SUITE_END()
这两个宏必须成对使用,宏之间的所有测试用例都属于这个测试套件。一个C++源文件中可以有任意多个测试套件,测试套件也可以任意嵌套,没有深度的限制。测试套件的名字一般以s 开头,例如:
BOOST_AUTO_TEST_SUITE (s_suitel) //测试套件开始
BOOST_AUTO_TEST_CASE (t_casel) "测试用例 1
{
BOOST_CHECK_EQUAL(1, 1);
... //其他测试断言
}
BOOST_AUTO_TEST_CASE (t_case2) //测试用例 2
{
BOOST_CHECK_EQUAL(5, 10/2);
... //其他测试断言
}
BOOST_AUTO_TEST_SUITE_END () //测试套件结束
任何一个UTF单元测试程序都必须存在一个主测试套件,它是整个测试树的根节点,其他的测试套件都是它的子节点。
主测试套件的定义可以使用宏BOOST_TEST_MAIN或者BOOST_TEST_MODULE,定义了这个宏的源文件中不需要再有宏BOOST_AUTO_TEST_SUITE 和 BOOST_AUTO_TEST_SUITE_ END,所有测试用例都自动属于主测试套件。
因此,6.3 .1小节的预编译文件test_main.cpp实际上不仅是预编译test库,它还定义了一个不包含任何测试用例的空主测试套件。
6.3.6 测试实例
了解了test库中测试用例和测试套件的概念,我们就可以进行真正的单元测试了。
首先要保证我们已经编译了 test库,方法如6.3.1小节所述,建立一个test_main.cpp,包含有test库的编译源码。这个文件就是test库的预编译源程序,含有一个空的主测试套件。一旦写好,不应该再变动它。
接下来我们新建一个cpp文件,它包含有我们的第一个测试套件,这里我们选择smart_ptr (参见3.1小节)作为我们的测试对象。测试套件的声明是:
BOOST_AUTO_TEST_SUITE(s_smart_ptr)
BOOST_AUTO_TEST_SUITE_END()
测试用例针对scoped_ptr和shared_ptr,像这样:
BOOST_AUTO_TEST_CASE(t_scoped_ptr)
{
//...
}
完整的单元测试程序如下:
#define BOOST_TEST_INCLUDED
#include <boost/test/unit_test.hpp>
#include <boost/smart_ptr.hpp>
using namespace boost;
//开始测试套件s_smart_ptr
BOOST_AUTO_TEST_SUITE(s_smart_ptr)
//测试用例1: t_scoped_ptr
BOOST_AUTO_TEST_CASE (t_scoped_ptr)
{
scoped_ptr<int> p(new int (874));
BOOST_CHECK(p); //p不是空指针
BOOST_CHECK_EQUAL(*p , 874); //测试解引用的值
p.reset(); //scoped_ptr 复位
BOOST_CHECK(p == 0); //为空指
}
//测试用例 2: t_shared_ptr
BOOST_AUTO_TEST_CASE(t_shared_ptr)
{
shared_ptr<int> p(new int (100));
BOOST_CHECK (p) ; //p 不是空指针
BOOST_CHECK_EQUAL(*p , 100) ; //测试解引用的值
BOOST_CHECK_EQUAL (p. use_count {) , 1); //引用计数为1
shared_ptr<int> p2 = p; //拷贝构造另一个shared_ptr
BOOST_CHECK_EQUAL(p, p2); //两个shared_ptr必定相等
BOOST_CHECK_EQUAL(p2.use_count(), 2); //引用计数为2
*p2 = 255; //改变第二个shared_ptr所指的内容
BOOST_CHECK_EQUAL(*p, 255); //第一个所指的内容也同时改变
BOOST_CHECK_GT(*p, 200);
}
//结束测试套件
BOOST_AUTO_TEST_SUITE_END()
单元测试程序的运行结果如下:
Running 2 test cases...
*** No errors detected
6.3.7测试夹具
测试用例和测试套件构成了单元测试的主体,可以满足大部分单元测试的功能需求,但有的时候这些还不够,因为它们不能完成测试安装和测试清理的任务。
测试安装执行测试前的准备工作,初始化测试用例或测试套件所需的数据。测试清理是测试安装的反向操作,执行必要的清理工作。
测试安装和测试清理的动作很像C++中的构造函数和析构函数,因此,可以定义一个辅助类, 它的构造函数和析构函数分别执行测试安装和测试清理,之后我们就可以在每个测试用例最开始声明一个对象,它将自动完成测试安装和测试清理。
基于这个基本原理,UTF中定义了“测试夹具”的概念,它实现了自动的测试安装和测试清理,就像是一个夹在测试用例和测试套件两端的夹子。测试夹具不仅可以用于测试用例,也可以用于测试套件和单元测试全局。
使用测试夹具,必须要定义一个夹具类,它只有构造函数和析构函数,用于执行测试安装和测试清理,基本形式是:
struct test_fixture_name //测试夹具类
{
test_fixture_name(){} //测试安装工作
~test_fixture_name(){} //测试清理工作
};
夹具类通常是个struct,因为它被UTF用于继承,测试套件可以使用它的所有成员。当然夹具类也可以是一个标准的class,有私有、保护和公开成员,但这样测试套件就只能访问夹具的保护和公开成员。指定测试用例和测试套件的夹具类需要使用另外两个宏:
#define BOOST_FIXTURE_TEST_SUITE( suite_name, F )
#define BOOST_FIXTURE_TEST_CASE( test_name, F )
它们替代了之前的 BOOST_AUTO_TEST_CASE 和 BOOST_AUTO_TEST_SUITE 宏,第二个参数指定了要使用的夹具类。
可以在测试套件级别指定夹具,这样套件内的所有子套件和测试用例都自动使用夹具类提供的安装和清理功能,但子套件和测试用例也可以另外自己指定其他夹具,不会受上层测试套件的影响。全局测试夹具需要使用另一个宏BOOST_GLOBAL_FIXTURE,它定义的夹具类被应用于整个测试模块的所有测试套件——包括主测试套件。示范测试夹具用法的代码如下,测试对象是assign库(参见4.4小节):
#define BOOST_TEST_INCLUDED
#include <boost/test/unit_test.hpp>
#include <boost/assign.hpp>
using namespace boost;
//全局测试夹具类
struct global__fixture
{
global_fixture() {cout << ("global setup\n") ; }
~global_fixture () { cout << ("global teardown\n") ; }
};
//定义全局夹具
BOOST_GLOBAL_FIXTURE(global_fixture);
//测试套件夹具类
struct assign_fixture
{
assign_fixture(){cout << ("suit setup\n");}
~assign_fixture() {cout << ("suit teardown\n");}
vector<int> v; //所有测试用例都可用的成员变量
};
//定义测试套件级别的夹具
BOOST FIXTURE TEST SUITE(s_assign, assign_fixture)
BOOST_AUTO_TEST_CASE(t_assignl) //测试+=操作符
{
using namespace boost::assign;
v += 1,2,3,4;
BOOST_CHECK_EQUAL(v.size(), 4);
BOOST_CHECK_EQUAL(v[2], 3);
}
BOOST_AUTO_TEST_CASE(t_assign2) //测试push_back函数
{
using namespace boost::assign;
push_back(v)(10)(20)(30);
BOOST_CHECK_EQUAL(v.empty(), false);
BOOST CHECK LT(v[0], v[1]);
}
BOOST__AUTO_TEST_SUITE_END () //测试套件结束
单元测试程序运行结果如下:
global setup
Running 2 test cases...
suit setup
suit teardown
suit setup
suit teardown
global teardown
*** No errors detected
6.3.8 测试曰志
测试日志是单元测试在运行过程中产生的各种文本信息,包括警告、错误和基本信息,默认情况下这些测试日志都被定向到标准输出(stdout)。测试日志不同于测试报告,后者是对测试日志的总结。
每条测试日志都有一个级别,只有超过允许级别的日志才能被输出。UTF的日志的级别从低到高,高级别禁止了低级别的许可,但比它更髙级别的日志则不受限制。
这些日志级别如下:
■ all : 输出所有的测试日志;
■ success : 相当于all;
■ test_suite : 仅允许运行测试套件的信息;
■ message : 仅允许输出用户测试信息(BOOST_TEST_MESSAGE);
■ warning : 仅允许输出警告断言信息(BOOST_WARN_XXX);
■ error : 仅允许输出CHECK、REQUIRE断言信息(BOOST_CHECK_XXX);
■ cpp_exception : 仅允许输出未被捕获的C++异常信息;
■ system_error : 仅允许非致命的系统错误;
■ fatal_error : 仅允许输出致命的系统错误;
■ nothing : 禁止任何信息输出
默认情况下,UTF的日志级别是warning,会输出大部分单元测试相关的诊断信息,但 BOOST_TEST_MESSAGE宏由于是message级别,它的信息不会输出。
日志级别可以通过接下来介绍的单元测试程序的命令行参数改变。
6.3.9运行参数
基于UTF的单元测试程序在编译完成后可以独立运行,test库提供了许多运行时的命令行参数,可以调整程序的运行状态,在测试大型程序时非常有用。
UTF的命令行参数基本格式是:
--arg_name=arg_value
参数名称和参数值都是大小写敏感的,并且==两边和__右边不能有空格。
常用的命令行参数如下:
■ run_test:可以指定要运行的测试用例或测试套件,用斜杠(/)来访问测试树的任意节点,支持使用通配符*;
■ build_info:单元测试时输出编译器、STL、boost等系统信息,取值为yes/no;
■ output_format:指定输出信息的格式,取值为hrf (可读格式)/xml;
■ log_format:指定日志信息的格式,取值为hrf (可读格式)/xml;
■ log_level:允许输出的日志级别。取值为all、success、test_suite、message、 warning、 error、cpp_exception、system_error、fatal_error、nothing,默认是warning;
■ show_progress:基于progress_display组件(参见2.4小节),显示测试的进度,取值为yes/no。不能显示测试用例内部的进度,只能显示已经完成的测试用例与测试用例总数的比例。
例如,对于6.3.7小节的单元测试程序,如果使用命令行参数:
--build_info=yes --run_test=s_assign/* --output_format=xml
程序运行结果可能是这样:
global setup
<TestLog>
<BuildInfo platform="Win32" compiler="Microsoft Visual C++ version 8.0" stl= "STLPort standard library version 0x521" boost="1.42.0"/>
suit setup
suit teardown
suit setup
suit teardown
global teardown
</TestLog>
<TestResult>
<TestSuite name="Master Test Suite’1 result="passed" assert ions_passed="4" assertions_failed="0" expected_failures="0" test_cases_passed="2" test_cases_failed="0" test_cases_skipped="0" test_cases_aborted="0">
</TestSuite>
</TestResult>
这段稍显“凌乱”的测试输出,以xml格式(--output_format=xml)显示了单元测试的运行系统信息(--build_info=yes),并运行了测试套件s_assign下的所有测试用例 (--run_test=s_assign/*)。
6.3.10 函数执行监视器
test库在UTF框架底层提供一个函数执行监视器类execution_monitor,它被UTF用于单元测试,但也可以被用于生产代码。execution_monitor可以监控某个函数的执行,即使函数发生预想以外的异常,也能够保证程序不受影响地正常运行,异常将会以一致的方式被 execution_monitor处理。
如果编译了test库,那么execution_monitor可以直接使用。如果不想仅仅为了使用 execution_monitor就编译庞大的test库,那么也可以单独编译execution_monitor部分。使用嵌入编译的方式需要定义如下的源文件:
//emprebuild.cpp
#include <boost/test/impl/execution_monitor.hpp>
#include <boost/test/impl/debug.hpp>
execution_monitor位于名字空间boost,在需要使用execution_monitor的地方应加入如下头文件声明:
#define BOOST_TEST_INCLUDED
#include <boost/test/execution_monitor.hpp>
using namespace boost;
用法
execution_monitor目前可以监控返回值为int或者可转换为int的函数,并需要使用 unit_test: : callback0<int>函数对象来包装(很遗憾,虽然execution_monitor也定义了用于包装多个参数和不同返回值类型的函数对象,但execute()成员函数只提供了参数为 unit_test: :callback0<int>),之后成员函数execute()就可以监控执行被包装的函数。
execution_monitor的监控执行语句需要嵌入在一个try-catch块里。如果一切正常,那么被execution_monitor监控执行的函数就像未被监控一样运行并返回。否则,如果发生了未捕获的异常、软硬件signal或trap、以及VC下的assert断言,那么execution_monitor就会捕获这个异常,重新抛出一个execution_monitor异常,它存储了异常相关的信息。
execution_exception不是标准库异常std: : exception的子类,必须在catch块明确地写出它的类型,否则execution_monitor抛出的异常不会被捕获。
示范execution_monitor用法的代码如下:
#define BOOST_TEST_INCLUDED .
#include <boost/test/execution_monitor.hpp>
using namespace boost;
int f() //一个简单的测试函数,必须是无参返回值为int
{
cout << "f execute." << endl;
throw "a error accoured"; //抛出一个未捕获的异常
return 10;
}
int main()
{
execution_monitor em; //声明一个监视器对象
try //开始 try-catch 块
{
em.execute(unit test::callback0<int>(f));//监控执行f
}
catch(execution_exception& e) //捕获异常
{
cout << "execution_exception" << endl;
cout << e.what().begin() << endl; //输出异常信息
}
}
请读者注意在上面的代码中对execution_exception异常的使用,在输出它存储的信息时我们使用了 e.what().begin()的方式。这是因为execution_ monitor被设计为在很少或者没有内存的情况下也可以使用,故它没有使用std::string来表示字符串,而是使用了一个内部类const_string。const_string默认不支持流输出,如果要使用流输出功能,需要包含头文件<boost/test/utils/basic_cstring/io.hpp>。
程序运行结果如下:
f execute.
execution_exception
C string: a error accoured
其他用法
除了基本的监控函数执行,execution_ monitor还有其他的用法。它提供了 p_timeout、 p_auto_start_dbg等读写属性用来设置监控器的行为,或者检测内存泄露,但这些功能不是完全可移植的,有的功能仅限于某些编译器或操作系统。execution_monitor也可以用于统一处理程序的异常,使用户不必自己编写错误处理代码。在这种情况下,程序抛出的异常类型必须是C字符串、std::string或者std::exception三者之一,才能够被execution_exception所处理,通常这能够适合大多数应用。如果因为某些原因必须使用自定义的异常类,而又想利用执行监控器的监控功能,那么可以定义异常类的变换函数,并注册到execution_monitor中。例如:
struct my_error //一个自定义异常类
{
int err code; //错误代码
my_error(int ec):err_code(ec){} //构造函数
};
void translate_my_err(const my_error& e) //变换函数
{
cout << "my err = " << e. err_code << endl; //输出到cout
}
int f() //被监控函数
{
cout << "f execute." << endl
throw my_error(100); //抛出自定义异常
return 0;
}
int main()
{
execution_monitor em;
//使用register_exception_translator函数注册异常变换函数
em.register_exception_translator<my_error>(&translate_my_err);
try //开始try-catch块,监控函数的执行
{
em.execute(unit_test::callback0<int>(f));
}
catch (const execution_exception& e)
{
cout << "execution_exception" << endl;
cout << e.what().begin();
}
}
程序的运行结果如下:
f execute.
my err = 100
6.3.11 程序执行监视器
test库在函数执行监视器execution_monitor的基础上提供程序执行监视器,它的目的与execution_monitor相似,监控整个程序的执行,把程序中的异常统一转换为标准的操作系统可用的错误返回码。
程序执行监视器的用法很像minimal test,只需要包含一个头文件,并实现与main()具有相同签名的cpp_main():
#include <boost/test/included/prg_exec_monitor.hpp>
int cpp_main( int argc, char* argvt] ){...}
注意:cpp_main()必须要返回一个整数,它不同于main(),不具有默认返回0值的能力。
程序执行监视器使用一个函数对象包装了cpp_main(),将它转换成一个返回int的无参函数对象,然后使用execution_monitor监控执行。因此它的行为基本上与execution_ monitor相同,当cpp_mai()发生异常或者返回非0值时就会被捕获,并把异常信息输出到屏幕上。
6.3.12 高级议题
boost.test库是一个复杂的系统,很难在这短短的几个小节中就讲述清楚,下面简单介绍一下test库的若干高级议题,供读者参考。
超轻量级测试
头文件<boost/detail/lightweight_test.hpp>包含一个未文档化的超轻量级单元测试工具lightweight_test,它甚至比minimal test还要小,功能也更少。
lightweight_test不是一个单元测试框架,它提供三个测试断言:
■ BOOST_TEST :相当于BOOST_CHECK,断言表达式成立;
■ BOOST_ERROR : 直接断言失败,输出一条错误消息;
■ BOOST_TEST_EQ :相当于BOOST_CHECK_EQUAL,断言两个表达式相等。
当临时要写测试代码、简单地验证程序正确性或者编译器不符合标准(无法使用test库的高级特性)时,lightweight_test特别有用,但它的局限性也很大,如果要进行正式的单元测试,则应该使用minimal test或者UTF。
lightweight_test不需要编译,也不需要特定的入口函数,测试断言可以用在程序的任何地方,就像使用assert—样。简单示范lightweight_test用法的代码如下:
#include <boost/smart_ptr.hpp>
#include <boost/detail/lightweight_test.hpp>
int main ()
{
shared_ptr<int> p(new int (10));
BOOST_TEST(*p == 10);
BOOST_TEST_EQ(p.use_count(), 1);
BOOST_ERROR ("error accored! !");
}
因为lightweight_test很小,因此简单地拷贝它所属的头文件lightweight_ test.hpp及所需的current_function.hpp,再修改一下源代码中的#include语句,就可以构造出一个便携的单元测试环境,在没有boost程序库的环境下也可以使用。
期望测试失败
通常情况下所有的测试断言都应当通过,但有的时候需要特定的测试失败,允许有少量的断言不通过。这时我们可以使用宏BOOST_AUTO_TEST_CASE_EXPECTED_FAILURES,它的声明如下:
#define BOOST_AUTO_TEST_CASE_EXPECTED_FAILURES( test_name, n )
在测试套件内部使用这个宏,指定测试用例名和允许失败的数量,例如:
BOOST_FIXTURE_TEST_SUITE(test_suit, fixture)
//允许出现两个断言失败
BOOST_AUTO_TEST_CASE_EXPECTED_FAILURES(t_casel, 2)
BOOST_AUTO_TEST_CASE(t_casel)
{...}
BOOST_AUTO_TEST_SUITE_END()
这个功能在程序还没有完成所有模块的功能时很有用,可以先写好单元测试代码,使用 BOOST_AUTO_TEST_CASE_EXPECTED_FAILURES 忽略未完成的功能代码测试。
手工注册测试用例
使用测试用例最简单方便的方式是使用宏BOOST_AUTO_TEST_CASE,它使用宏参数生成一个同名的无参函数,可以在里面编写测试代码。但手工注册也有好处,可以任意编写测试用函数或者接受一些测试用的数据,最后只需要写少量的代码就可以注册到UTF,实现测试代码与注册的分离,有利于代码的维护。
手工注册需要使用宏BOOST_TEST_CASE和BOOST_PARAM_TEST_CASE生成测试用例类,并调用framework::xxx_test_suite().add()方法,本书不作详细的介绍,读者可参考 boost库文档。
测试泛型代码
UTF也能够测试模板函数和模板类,这对于泛型库开发是非常有用的。手工注册测试用例需要使用两个宏:BOOST_TEST_CASE_TEMPLATE_FUNCTION,用于定义测试用例模板主体; BOOST_TEST_CASE_TEMPLATE,基于前者创建并注册测试用例。
如果采用自动测试方式,可使用BOOST_AUTO_TEST_CASE_TEMPLATE,它需要使用模板元编程库mpl,声明如下:
#define BOOST_AUTO_TEST_CASE_TEMPLATE( test_name, type_name, TL )
简单示范泛型单元测试用法的代码如下,测试对象是lexical_cast(参见5.1小节):
#define BOOST TEST INCLUDED
#include <boost/test/unit_test.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/mpl/list.hpp>
using namespace boost;
BOOST_AUTO_TEST_SUITE(s_lexical_cast)
typedef mpl::list<short, int, long> types; //模板元编程类型容器
BOOST_AUTO_TEST_CASE_TEMPLATE(t_lexical_cast, T, types)
{
T n(20);
BOOST_CHECK_EQUAL (lexical_cast<string> (n) , "20");
}
BOOST_AUTO_TEST_SUITE_END()
VC下使用test库的一个小技巧
test在文档中向库用户提供了一个在VC系列开发环境使用test库的一个小技巧,可以在工程选项build-event中设置post-build,加入命令:
"$ (TargetDir) \$ (TargetName) . exe" --result_code=no --report_level=no
这样在VC编译完成后可以立刻运行单元测试,并且在Output窗口显示出未通过的测试断言,可以用双击的方式快速跳转到断言的位置,提高测试的工作效率。
6.4 总结
很多软件方法学如敏捷开发、极限编程都非常强调测试的重要性,使用boost测试库,我们就可以实践这些方法。
assert库提供一个assert宏的增强版本BOOST_ASSERT,它默认的行为与assert相同,但其规范的大写形式有助于代码的维护。如果定义了配置宏BOOST_ENABLE_ASSERT_ HANDLER, BOOST_ASSERT就给断言安装了一个处理handle,能够以任意方式处理断言失败的情形。
static_assert是编译期的断言,计算编译期表达式的值,在泛型编程和模板元编程领域非常有用。如果读者正在使用模板技术编写泛型算法或者泛型类,那么应该仔细地研究一下它的用法,它可以保证程序中的模板函数或者模板类如预期那样工作。
本章的重点内容是test库,它实现了一个完整的单元测试框架UTF,不仅可以测试普通的函数和类,也可以测试模板函数和模板类,这是其他类似功能的单元测试工具所示具备的。test库必须编译后才能使用,但可以釆用嵌入工程编译的方法以获得系统独立性。
最简单最容易使用的单元测试是minimal test,它不需要编译就可以使用,提供UTF最基本的测试功能,很容易使用,但不适合大多数实际的软件开发项目。
UTF提供了完整的单元测试框架,提供各种测试概念,如测试安装/清理、测试套件、测试用例、测试断言,用法非常自由,支持手工和自动两种向框架注册的方式,自动方式在通常情况下是最佳的选择,可以简化测试代码的编写工作。
UTF在测试的输出方面也很灵活,输出日志有详细的级别设定,通过运行参数可以指定允许输出的级别,日志格式也可以选择普通的HRF(Human Readable Format)或XML格式。运行参数还能够控制单元测试的其他方面,如输出系统消息、显示测试进度等等。
除了单元测试,test库还提供了用于生产环境的执行监视器,保证不抛出异常,并对外提供统一的异常处理方式。