Boost.Test
个人使用总结
在使用Boost.Test之前,先看一下多个测试单元的header-only用法方式。要不然编译器报错的时候会一脸懵逼,说实话现在[2020/08/02]我觉得Boost.Test没有googletest那么好用,但是想去除点库依赖。没办法,凑合着慢慢使用吧。
介绍
测试任何可能出错的东西 --极限编程(XP)格言
测试验收让客户对软件提供的商业价值满意,并愿意为它付钱。而单元测试使软件开发者对软件的功能有信心。 --极限编程(XP)格言
当你开始编写新的库/类/程序时,您需要做的第一件事是什么? 没错-您需要从单元测试模块开始(希望大家都给出了答案!)。你可能使用“asserts”就可以了,但专业的开发者很快就发现这样做的不足。 显然,对于简单但重复的单元测试任务而言,它既费时又乏味,而对于大多数不重要的任务而言则过于僵化。
Boost.Test库提供了一组易于使用且灵活的接口,用于编写测试程序,将测 试组织成简单的测试用例和测试套件以及控制其运行时执行。 一些Boost.Test的接口在生产(非测试)环境中也很有用。
入门示例,这是最小的单文件测试程序的样子:
//宏BOOST_TEST_MODULE定义了程序的名称,该名称将在消息中使用。
#define BOOST_TEST_MODULE My Test
//这引用了“header-only”模式的所有单元测试框架;它甚至定义了main函数,它将调用随后定义的测试用例。
#include <boost/test/included/unit_test.hpp>
//宏BOOST_AUTO_TEST_CASE声明了一个名为first_test的测试用例,该用例随后将在受控测试环境中运行first_test的内容。
BOOST_AUTO_TEST_CASE(first_test)
{
int i = 1;
BOOST_TEST(i); //此测试检查i是否为非零。
BOOST_TEST(i == 2);//此测试检查i是否具有值2(不仅仅评估相等运算符)。
}
运行时,将产生以下输出:
Running 1 test case...
test_file.cpp(8): error: in "first_test": check i == 2 has failed [1 != 2]
*** 1 failure is detected in the test module "My Test"
设计原理
单元测试任务出现在软件开发的许多不同阶段:从最初的项目实施到维护和后来的修订。这些任务的复杂性和目的不同,因此不同的开发人员对它们的处理方式也不同。问题域中的各种任务导致对单元测试框架提出许多要求(有时会产生冲突)。这些包括:
- 对于新用户来说,编写单元测试模块应该很简单并且显而易见。
- 该框架应允许高级用户执行复杂的测试。
- 测试模块应该能够包含许多小的测试用例,开发人员应该能够将它们分组到测试套件中。
- 在开发开始时,用户希望看到详细的描述性错误消息。
- 在回归测试期间,用户只想知道是否有任何测试失败。 对于小型测试模块,其执行时间应优先于编译时间:用户不想等待一分钟来编译运行仅需要一秒钟的测试。
- 对于耗时长而复杂的测试,用户希望能够看到测试进度。
- 最简单的测试不需要外部库。
- 对于长期使用,单元测试框架的用户应该能够将其构建为独立的库。
单元测试框架满足上述要求,并提供通用的功能来:
- 在测试的代码中轻松指定所有期望。
- 将这些期望组织到测试用例和测试套件中。
- 检测不同类型的错误,故障,超时并以统一的可定制方式报告它们。
为什么需要框架?虽然您可以从头开始编写测试程序,但该框架具有以下优点:
- 您会收到文本格式的错误报告。错误报告是统一的,您可以轻松地对其进行机器分析。
- 错误报告与测试代码分开。您可以轻松更改错误报告格式,而不会影响测试代码。
- 该框架自动检测被测试的组件抛出的异常和超时,并报告其他错误。
- 您可以轻松过滤测试用例,并仅调用所需的用例。这不需要更改测试代码。
如何阅读本文档
本文档由您作为用户所需要了解的内容构成,以便成功使用单元测试框架以及必须做出的决策顺序和可能遇到的问题的复杂程度。如果您发现自己面对一些不清楚的术语,请直接跳转至术语表部分,该术语表部分收集了所有使用过的术语的简短定义。
通常,在使用单元测试框架编写测试模块时,您必须执行以下步骤:
- 您决定如何合并单元测试框架:#include只有头文件的库,或将其链接为静态库,或将其用作共享(或动态加载)库。有关此主题的详细信息,请参见用法变量部分。
- 您将测试用例添加到测 试树中。有关详细信息,请参见测试用例部分。
- 您对被测代码进行正确性检查。有关详细信息,请参见编写单元测试部分。
- 您需要在每个测试用例之前执行被测代码的初始化。有关详细信息,请参见“Fixtures(夹具)”一节。
- 您可能想要自定义报告测试失败的方式。有关详细信息,请参见控制输出部分。
- 您可以控制内置测试模块的运行时行为(例如,仅运行选定的测试,更改输出格式)。这将在运行时配置一节中介绍。
如果您在上述任何部分中都找不到答案,或者您认为需要更多配置选项,则可以查看“高级使用方案”部分。
使用方式
单元测试框架支持三种不同的使用方式:
- header-only
- 静态链接库
- 动态链接库
在大多数情况下,使用哪一种方式都不会有问题,有明确的理由将告知你为什么要使用其中一种方式。 以下各节将帮助您做出决定。
-
仅头文件(header-only)使用方式
如果您希望避免编译独立库,则应使用单元测试框架的仅头文件的变量。 此变量仅要求您引用唯一的头文件:
#include <boost/test/included/unit_test.hpp>
,无需链接任何库。 有几种执行初始化的方法,但是最简单的方法如下:#define BOOST_TEST_MODULE test module name
#include <boost/test/included/unit_test.hpp>BOOST_TEST_MODULE
宏需要在包含头文件之前定义,并应指示测试模块的名称。 此名称可以包含空格,不需要用引号引起来。自定义header-only使用方式 这一节提供有关如何自定义此使用变量的其他详细信息。 特别是,可以有多个带有此变量的编译单元,正如header-only多个测试单元文件中所述。
-
静态库使用方式
对于大多数有权访问单元测试框架的静态库[1]或可以自行构建的用户而言,接下来的用法可能是最通用和最简单的方法。 此用法变体需要两个步骤。
-
首先,需要将以下行添加到测试模块中的所有测试单元:
#include <boost/test/unit_test.hpp>
一个且只有一个测试单元应包括以下几行:
#define BOOST_TEST_MODULE test module name
#include <boost/test/unit_test.hpp>BOOST_TEST_MODULE宏需要在包含之前定义,并应指示测试模块的名称。此名称可以包含空格,不需要用引号引起来。
-
第二步是与单元测试框架静态库链接。
注意:头文件
<boost/test/unit_test.hpp>
是一个聚合头文件:它包括大多数其他包含单元测试框架定义的头文件。此用法变量的另一面是,此用法变量之后的每个测试模块都将与单元测试框架静态链接,这可能是您要避免的事情(例如,以节省空间)。有关这些配置选项的更多信息,请参考 此小节。
-
-
共享库使用方式
在具有大量测试模块的项目中,单元测试框架的静态库方式可能会导致您浪费大量磁盘空间。解决方案是将测试模块与构建为共享库的单元测试框架动态链接。此用法变体需要两个步骤。
-
首先,您需要向测试模块中的所有测试单元添加以下行:
#define BOOST_TEST_DYN_LINK
#include <boost/test/unit_test.hpp>并且只有一个测试单元应包括以下几行
#define BOOST_TEST_MODULE test module name
#define BOOST_TEST_DYN_LINK
#include <boost/test/unit_test.hpp>
-
需要在包含之前定义BOOST_TEST_MODULE和BOOST_TEST_DYN_LINK宏。 BOOST_TEST_MODULE应该设置为测试模块名称。 此名称可以包含空格,不需要用引号引起来。
- 第二步是与单元测试框架共享库链接。
这种使用方式的另一面是,您需要确保单元测试框架共享库在运行时可被测试模块访问。
此外,共享库用法变体有助于自定义测试运行程序。 有关此的更多信息,请检查 此部分.。
注意:在Windows上,测试模块和单元测试框架共享库应链接到同一CRT。 不这样做(例如,当测试模块处于调试状态时,处于发布模式的单元测试框架共享库)将导致崩溃。
声明和组织测试
如果您查看许多旧式测试模块,则很有可能将其实现为包含检查语句和输出语句的一个大型测试功能。这有什么问题吗?是。单一测试功能方法有很多缺点:
- 如果检查数量超过合理的限制(对任何大型功能都是如此),那么一个大型功能往往会变得很难管理。哪些测试s是通过了还是没通过,在哪里,谁知道?
- 许多检查需要类似的准备。这导致测试函数内的代码重复。
- 如果测试函数中的任何检查导致致命错误或异常,则将跳过其余测试,并且无法防止这种情况。
- 无法仅对被测单元的特定子系统执行检查。
- 没有关于测试中测试单元的不同子系统如何执行的摘要。
以上几点应明确表明,最好将测试模块拆分为较小的测试单元。这些单元是测试用例,测试套件(Test Suites )和测试夹具(Fixtures)。
本节涵盖的主题
-
声明(Declaration):单元测试框架支持几种声明测试用例的方法。测试用例可以使用语法上形似非成员函数(Free Function)或基于实际的非成员函数,函数对象,这些函数可以定义为有无参数/数据的函数。或作为针对各种类型运行的模板函数来实现。
-
组织(Organization):单元测试框架提供了将多个测试用例分组到测试套件中的功能。测试套件可以嵌套,并且测试套件和测试用例一起被定义测试树,其中叶子节点是测试用例。除了层次结构外,单元测试框架还允许您使用逻辑分组和依赖性来组织测试树,并为您提供您想要的方式(例如,通过命令行)来使用测试树组织。
-
属性:可以使用装饰器指定测试单元属性。这些属性用于对测试模块执行的各个方面进行精细控制,例如逻辑分组,依赖关系,预期的失败等。
-
创建/清除(Setup/Teardown)测试单元动作:当多个测试共享同一设置(环境,测试数据准备等)时,创建 和清理代码可以在在测试夹具中分解。在单元测试框架中,测试夹具可以与测试用例,测试套件关联,也可以与测试模块全局关联。
测试用例
测试用例是由测试运行程序运行的执行单元。它包含指令和声明,并且其执行由单元测试框架进行监视。记录有关执行的信息,并生成日志/报告。
应该将测试用例通知测试运行者以便运行它:应该将测试用例注册为包含在测试树中。
单元测试框架涵盖以下测试用例场景:
- 没有参数的测试用例:这些类似于在测试运行程序的受控环境中运行功能。
- 带参数的测试用例:此用法旨在运行可能具有许多不同参数的相同函数,每个具有不同参数的调用均由测试运行程序处理。
- 在模板上测试案例:该场景是针对几种类型测试同一模板实现。
对于上述每种情况,测试用例都有不同的声明API。首选的API将声明测试用例并在测试树中自动注册它,而无需执行手动注册。
手动注册
虽然自动注册是首选的测试用例声明API,但也可以手动声明测试。对于此API,单元测试框架选择了基于通用回调方法的最少侵入性设计,该签名取决于所声明的测试用例的关键。
单个测试模块可以混合自动和手动测试用例注册。换句话说,在同一测试模块中,您既可以远程实现测试用例,也可以在测试模块初始化功能中手动注册它们,也可以在实现点自动注册测试用例。
注意:单元测试框架中的手动测试用例声明API的设计假定测试用例实现(测试功能主体)且测试用例创建/注册点是远程 的。结果,您可能会忘记注册测试用例,即使它存在于测试文件中,也永远不会执行。
在选择使用手动注册之前,您需要确保先用尽所有可能的方法来使用自动注册API。特别:
- 如果需要(可选)包括/排除一些测试用例,请考虑改用enabled / disabled / enable_if装饰器
- 如果您需要基于某些数据注册一些参数化的测试用例,请考虑使用数据驱动的测试用例
- 如果您需要指定复杂的测试单元依赖关系,则可以使用depends_on装饰器
- 如果您需要在测试单元之间共享逻辑,请考虑使用夹具(fixtures)装置
没有参数的测试用例
最常见的情况是您要编写没有任何参数的测试用例。 单元测试框架为您提供了自动和手动注册API来声明此类测试用例。
数据驱动的测试用例
为什么使用数据驱动的测试用例?需要针对一系列不同的输入参数重复某些测试。 实现此目的的一种方法是为每个参数手动注册一个测试用例。 您还可以从测试用例中用所有的测试参数来调用测试函数,如下所示:
void single_test( int i )
{
BOOST_TEST( /* test assertion */ );
}
void combined_test()
{
int params[] = { 1, 2, 3, 4, 5 };
std::for_each( params, params+5, &single_test );
}
上面的方法有几个缺点:
- 运行测试的逻辑是在测试内部:上例中的
single_test
是从测试用例combined_test
运行的,但是单元测试框架能够更好地处理其执行情况。 - 如果上述param数组中的值之一发生严重错误(例如
BOOST_TEST_REQUIRE
中的错误),则终止测试combined_test
并执行测试树中的下一个测试用例。 - 万一发生故障,报告不够准确:在调试会话期间,当然应该由人工来重新执行测试,或者应该在测试本身中实现其他报告逻辑。
参数生成、可扩展性和组合
在某些情况下,人们希望对任意大的一组值运行参数化测试。 手动枚举参数不是一个很好扩展的解决方案,尤其是当这些参数可以在另一个生成这些值的函数中定义生成时。 但是,这个解决方案也有局限性
-
生成函数:假设我们有一个函数
func(float f)
,其中f
是[0,1]
中的任何数字。我们对确切的值不太感兴趣,但我们想测试func
。怎么样,我们不是编写将测试func
的f
,而是在[0,1]
中随机选择f
? 另外,如果f
不是只有一个值,而是对任意多个数字进行测试呢?从这个小例子中我 们很容易理解,当提供生成函数而不是在测试中写下常量值时,接受参数的测试会更强大。 -
可伸缩性:假设我们有一个测试用例测试
func1
,我们在该用例上测试在测试文件中写入为常量的N个值测试能保证什么? 我们保证func1
正在处理这N
个值。 然而,在这种情况下,N
必然是有限的并且通常很小。 我们如何轻松地对N
进行伸缩?一种解决方案是能够生成新值,并能够针对func1
的可能输入类别定义测试,该函数在该类别上应该具有已定义的行为。在某种程度上,测试中记下的N
常量只是func1
可能输入的摘录,处理输入类别为测试提供了更大的灵活性和功能。 -
组合:假设我们已经有两个函数
func1
和func2
的测试用例,分别以类型T1
和T2
作为参数。现在,我们要测试一个新函数func3
,该函数T3
类型作为参数,其包含T1
和T2
,并通过已知算法调用func1
和func2
。这样的设置的一个例子是// Returns the log of x
// Precondition: x strictly positive.
double fast_log(double x);
// Returns 1/(x-1)
// Precondition: x != 1
double fast_inv(double x);
struct dummy {
unsigned int field1;
unsigned int field2;
};
double func3(dummy value) {
return 0.5 * (exp(fast_log(value.field1))/value.field1 +
value.field2/fast_inv(value.field2));
}在这个例子中
func3
继承于fast_log
和fast_inv
的先决条件:它分别在 (0, +infinity) 和 [-C, +C] - {1} 中为 field1 和 field2 定义(C 是一个任意大的常数)。- 如上所定义,func3在其定义域中的所有位置都应接近1。
- 我们想在复合函数
func3
中重用fast_log
和fast_inv
的属性,并断言func3
是在任意大定义域上定义良好。
在
func3
上进行参数化测试几乎不能告诉我们接近点{field1 = 0,field2 = 1}
的可能数值属性或不稳定性。实际上,参数化测试可能会测试 (0,1) 附近的某些点,但无法提供接近该点的函数的渐近行为。
Boost.Test框架中的数据驱动测试
单元测试框架提供的工具解决了上述问题:
- 数据集的概念简化了测试用例的输入类别的描述。数据集还实现了多种操作,使它们的组合能够创建新的,更复杂的数据集,
- 两个宏
BOOST_DATA_TEST_CASE
和BOOST_DATA_TEST_CASE_F
分别具有和不具有测试夹具的支持,用于声明和注册一组值(样本)中的测试用例, - 与唯一值关联的每个测试用例均独立于其他执行。这些测试以与常规测试用例相同的方式进行保护,这 使得对数据集的每个样本的测试执行隔离,健壮,可重复且易于调试,
- 单元测试框架提供了几个数据集生成函数
本节的其余部分涵盖了单元测试框架提供的有关数据驱动测试用例的概念和功能,尤其是:
- 介绍了数据集和样本的概念
- 解释了数据驱动测试用例的声明和注册,
- 详细的数据集操作
- 最后介绍了内置的数据集生成器。
参数化测试用例
此功能已被数据驱动的测试用例工具取代。
测试树
测试树是测试用例和测试套件的层次结构,所有的测试夹具(全局,用例或套件级别),以及所有这些元素内的依赖关系。
测试树由以下内容组成:
- 测试用例:这些是树中包含测试内容的元素,它们构成树的叶子节点。
- 测试套件:这些是树的内部节点。 这些元素本身没有任何测试内容或可执行代码,但是可以将执行代码和测试夹具附加到它们上。
- 主测试套件:这是树的根,并且定义为测试套件。 附属于主测试套件的测试夹具是全局测试夹具。
- 测试夹具:是在上述测试单元之前和/或之后执行的代码单元。
以下层次结构表示没有任何测试夹具的测试树(在测试套件部分中进一步详细介绍):
除主测试套件外,还可以将**装饰器(Decoration)**添加到测试套件和用例中。 这些装饰器可能会修改单元测试 框架处理测试树的方式。 例如,由测试树本身执行的测试用例的执行没有定义的顺序,除了测试夹具和它们所涉及的元素(套件,用例); 装饰器可以用来指示测试树元素之间的特定顺序。
注意:在执行测试用例时,测试树本身不给出任何特定顺序。 唯一的隐式顺序由测试夹具给出。 为了指示特定的顺序,应使用特定的装饰器。
测试套件
如果您将测试用例视为测试树上的叶子节点,则可以将测试套件视为分支节点,而将主测试套件视为根节点。但是,与真正的树不同,我们的树在许多情况下只包含直接附着在根节点上的叶子节点。所有测试用例都直接驻留在主测试套件中是很常见的。如果您确实想构建分层的测试套件结构,则单元测试框架将提供手动和自动的测试套件创建和注册功能:
- 自动注册的测试套件
- 手动注册的测试套件
此外,单元测试框架还提出了主测试套件的概念。了解此组件的最重要原因是,它提供了访问提供给测试模块的命令行参数的功能。
自动注册
单元测试框架为自动化测试套件创建和注册提供的解决方案旨在促进多点定义、任意测试套件深度以及与自动化测试用例创建和注册的平滑集成。 与手动显式注册相比,该功能应明显能够简化测试树构建过程。
该实现基于单个编译单元中文件范围变量定义的顺序。此功能的语义与C ++的名称空间特性非常相似,包括对测试套件扩展的支持。要启动测试套件,请使用宏BOOST_AUTO_TEST_SUITE
。要结束测试套件,请使用宏BOOST_AUTO_TEST_SUITE_END
。可以 在同一个测试文件或不同的测试文件中多次重新启用同一个测试套件。结果,所有测试单元将成为已构建测试树中同一测试套件的一部分。
BOOST_AUTO_TEST_SUITE(test_suite_name);
BOOST_AUTO_TEST_SUITE_END();
在测试套件开始和结束声明之间定义的测试单元将成为测试套件的成员。 测试单元始终成为所声明的最接近的测试套件的成员。 在测试文件范围内声明的测试单元将成为主测试套件的成员。 对测试套件包含的深度没有限制。
本示例创建一个与手动测试套件注册示例中创建的树完全匹配的测试树。
示例:自动注册的测试套件
#define BOOST_TEST_MODULE example
#include <boost/test/included/unit_test.hpp>
BOOST_AUTO_TEST_SUITE( test_suite1 )
BOOST_AUTO_TEST_CASE( test_case1 ) {
BOOST_TEST_WARN( sizeof(int) < 4U );
}
BOOST_AUTO_TEST_CASE( test_case2 ) {
BOOST_TEST_REQUIRE( 1 == 2 );
BOOST_FAIL( "Should never reach this line" );
}
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_SUITE( test_suite2 )
BOOST_AUTO_TEST_CASE( test_case3 ) {
BOOST_TEST( true );
}
BOOST_AUTO_TEST_CASE( test_case4 ) {
BOOST_TEST( false );
}
BOOST_AUTO_TEST_SUITE_END()
输出
> example
Running 4 test cases...
test.cpp(21): fatal error: in "test_suite1/test_case2": critical check 1 == 2 has failed [1 != 2]
test.cpp(35): error: in "test_suite2/test_case4": check false has failed
*** 2 failures are detected in the test module "example"
如您所见,此示例中的测试树构造更加简单和自动化。
在下面的示例中,测试套件test_suite
由两部分组成。 它们的定义是远程的,并由另一个测试用例分隔。 实际上,这些部分甚至可以驻留在不同的测试文件中。 生成的测试树保持不变。 从输出中可以看到,test_case1和test_case2都位于同一测试套件test_suite中。
示例:自动注册功能的扩展测试套件
#define BOOST_TEST_MODULE example
#include <boost/test/included/unit_test.hpp>
BOOST_AUTO_TEST_SUITE( test_suite )
BOOST_AUTO_TEST_CASE( test_case1 ) {
BOOST_ERROR( "some error 1" );
}
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_CASE( test_case_on_file_scope ) {
BOOST_TEST( true );
}
BOOST_AUTO_TEST_SUITE( test_suite )
BOOST_AUTO_TEST_CASE( test_case2 ) {
BOOST_ERROR( "some error 2" );
}
BOOST_AUTO_TEST_SUITE_END()
输出
>example --report_level=detailed
Running 3 test cases...
test.cpp(8): error in "test_case1": some error 1
test.cpp(23): error in "test_case2": some error 2
Test suite "example" failed with:
1 assertion out of 3 passed
2 assertions out of 3 failed
1 test case out of 3 passed
2 test cases out of 3 failed
Test suite "test_suite" failed with:
2 assertions out of 2 failed
2 test cases out of 2 failed
Test case "test_case1" failed with:
1 assertion out of 1 failed
Test case "test_case2" failed with:
1 assertion out of 1 failed
Test case "test_case_on_file_scope" passed with:
1 assertion out of 1 passed
手动注册的测试套件
要手动创建测试套件,您需要
- 创建
boost::unit_test::test_suite
类的实例, - 在测试树中注册它,并
- 用测试用例(或较低级别的测试套件)填充它。
测试单元注册接口
单元测试框架使用类boost::unit_test::test_suite
为测试用例容器(测试套件)的概念建模。 有关完整的类接口参考,请参见本文档的高级部分。 在这里,您应该只对单个测试单元注册接口感兴趣:
void test_suite::add(test_unit *tc,counter_t expected_failures = 0, int timeout = 0 );
第一个参数是指向新创建的测试单元的指针。第二个可选参数expected_failures
定义在测试单元中预期失败的测试断言的数量。默认情况下,不会出现任何错误。
注意:为测试套件提供许多预期的故障时,请务必小心。默认情况下,单元测试框架将测试套件中的预期故障数计算为构成它的所有测试单元中适当值的总和。改变这一点很少有意义。
第三个可选参数timeout
定义测试单元的超时值。到目前为止,单元测试框架无法为测试套件执行设置超时,因此该参数仅对测试用例注册有意义。默认情况下,未设置超时。有关超时值的更多详细信息,请参见方法boost::unit_test::test_suite::add
。
为了在一个函数调用中注册一组测试单元,test_suite
类提供了本文档高级部分中介绍的另一个add
接口。
测试套件实例构造
要手动创建测试套件实例,请使用宏BOOST_TEST_SUITE
。它隐藏了所有实施细节,您只需要指定测试套件名称即可:
BOOST_TEST_SUITE(test_suite_name);
BOOST_TEST_SUITE
创建一个boost::unit_test::test_suite
类的实例,并返回一个指向所构造实例的指针。 另外,您可以自己创建一个boost::unit_test::test_suite
类的实例。
注意: boost::unit_test::test_suite
实例必须在堆上分配,并且编译器不允许您在栈上创建实例。
新创建的测试套件必须使用add接口在父级套件中注册。 测试套件的创建和注册都在测试模块初始化功能中执行。
下面的示例创建一个测试树,该树可以由以下层次结构表示:
示例:手动注册的测试套件
#include <boost/test/included/unit_test.hpp>
using namespace boost::unit_test;
void test_case1() { /* ... */ }
void test_case2() { /* ... */ }
void test_case3() { /* ... */ }
void test_case4() { /* ... */ }
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;
}
输出
> example --log_level=test_suite
Running 4 test cases...
Entering test suite "Master Test Suite"
Entering test suite "test_suite1"
Entering test case "test_case1"
Leaving test case "test_case1"
Entering test case "test_case2"
Leaving test case "test_case2"
Leaving test suite "test_suite1"
Entering test suite "test_suite2"
Entering test case "test_case3"
Leaving test case "test_case3"
Entering test case "test_case4"
Leaving test case "test_case4"
Leaving test suite "test_suite2"
Leaving test suite "Master Test Suite"
*** No errors detected
主测试套件
测试命名
测试树内容
夹具
一般而言,测试夹具或测试上下文是执行测试所需的以下一项或多项的集合:
- 前提条件
- 被测单元的特定状态
- 必要的清理程序
尽管在许多(即使不是全部)测试用例中都会遇到这些任务,但是使测试夹具与众不同的是重复。在正常的测试用例实现本身可以完成所有准备和清除工作的地方,测试夹具允许将其实现在单独的可重用单元中。
随着eXtreme编程(XP)的引入,要求测试设置/清除重复的测试风格变得越来越流行。单个XP采用的测试模块可能包含数百个单个断言测试用例,其中许多需要非常相似的测试设置/清除。这是测试夹具设计要解决的问题。
实际上,测试夹具通常是设置(setup)和拆卸(teardown)函数的组合,与测试用例相关。前者用于测试设置。后者专门用于清理任务。理想情况下,我们希望测试模块作者能够定义在堆栈上的固定夹具中使用的变量,并同时在测试用例中直接引用它们。
重要的是要理解C ++提供了一种实现简单的测试夹具解决方案的方法,该解决方案几乎可以满足我们的要求,而无需测试框架的任何额外支持。这是具有这种固定夹具的简单测试模块可能看起来像:
struct MyFixture {
MyFixture() { i = new int; *i = 0 }
~MyFixture() { delete i; }
int* i;
};
BOOST_AUTO_TEST_CASE( test_case1 ) {
MyFixture f;
// do something involving f.i
}
BOOST_AUTO_TEST_CASE( test_case2 ) {
MyFixture f;
// do something involving f.i
}
这是一种通用解决方案,可用于实施任何类型的共享设置或清除过程。 这种基于C ++的固定夹具解决方案仍然存在一些或多或少的实际问题:
- 我们需要手动在每个测试用例中添加一个夹具声明语句。
- 夹具中定义的对象是带有
<fixture-instance-name>
前缀的引用。 - 没有地方可以执行全局夹具,该夹具在测试之前和之后执行全局设置/清除程序。
单元测试框架使您可以根据几个通用接口定义夹具,从而帮助您完成以下任务:
- 为单个或一组测试用例定义共享的设置/拆卸程序
- 定义设置/拆卸程序,每个测试套件执行一次
- 定义全局设置/拆卸程序,每个测试模块执行一次
夹具模型
单元测试框架支持多个夹具接口。 接口的选择主要取决于夹具的用途。
夹具类模型
单元测试框架定义通用夹具类模型如下:
struct <fixture-name>{
<fixture-name>(); // setup function
~<fixture-name>(); // teardown function
};
换句话说,期望将夹具实现为一个类,其中类构造函数用作设置函数,而类析构函数用作拆卸函数。
上面的类模型有一些局限性:
- 拆卸函数中不可能有异常,尤其是任何中止当前测试用例的测试断言都是不可能的(因为那些使用断言的异常)
- 有时使用构造函数/析构函数执行夹具的必要资源分配/释放会更自然,这将在测试用例中使用,并在单独的功能中检查夹具的正确状态。 这些检查是测试用例运行的前提条件,也是测试用例运行后应满足的条件。
这就是单元测试框架还支持(将Boost 1.65启用)可选设置和/或拆卸函数的原因,如下所示:
struct <fixture-name>{
<fixture-name>(); // ctor
~<fixture-name>(); // dtor
void setup(); // setup, optional
void teardown(); // teardown, optional
};
注意:如前所述,
setup
和teardown
的声明/实现是可选的:单元测试框架将检查它们的存在并调用它们。 但是,在C++ 98中,如果继承了那些声明,则不可能检测到这些声明(对于支持auto和decltype的编译器,它工作正常)。
这些夹具模型可以与BOOST_FIXTURE_TEST_CASE和BOOST_FIXTURE_TEST_SUITE一起使用。
测试用例夹具
测试用例夹具是测试用例消耗的夹具:夹具setup
在测试用例执行之前被调用,夹具teardown
在测试用例完成执行之后被调用,而与执行状态无关。
单元测试框架提供了几种定义测试用例的方法,每种方法都有其属性:
- 单个测试用例的夹具声明,让测试用例访问夹具的成员,
- 为一个测试用例声明一个或多个夹具,而无需访问成员并具有灵活的接口,
- 由子树定义的一组测试用例的夹具的声明,可以访问夹具的成员。
单测试用例夹具
可以使用以下两种方法来声明连接到一个特定测试用例的夹具:
- 使用宏
BOOST_FIXTURE_TEST_CASE
代替BOOST_AUTO_TEST_CASE
,该宏允许访问夹具的成员 - 装饰器
fixture
的使用,不允许访问成员,但可以为一个测试用例定义多个夹具。
夹具与BOOST_FIXTURE_TEST_CASE
BOOST_FIXTURE_TEST_CASE
用作带有夹具的测试用例声明,旨在代替BOOST_AUTO_TEST_CASE
的测试用例声明:
BOOST_FIXTURE_TEST_CASE(test_case_name, fixture_name);
与宏BOOST_AUTO_TEST_CASE
的唯一区别是存在一个额外的参数fixture_name
。 可以从测试用例主体直接访问夹具的公共成员和受保护成员。 测试用例只能安装一个夹具。
您无法访问Fixture的私有成员,但是为什么要声明任何私有成员呢?
示例:每个测试用例夹具
#define BOOST_TEST_MODULE example
#include <boost/test/included/unit_test.hpp>
struct F {
F() : i( 0 ) { BOOST_TEST_MESSAGE( "setup fixture" ); }
~F() { BOOST_TEST_MESSAGE( "teardown fixture" ); }
int i;
};
BOOST_FIXTURE_TEST_CASE( test_case1, F ) {
BOOST_TEST( i == 1 );
++i;
}
BOOST_FIXTURE_TEST_CASE( test_case2, F ) {
BOOST_CHECK_EQUAL( i, 1 );
}
BOOST_AUTO_TEST_CASE( test_case3 ) {
BOOST_TEST( true );
}
example --log_level=message
Running 3 test cases...
setup fixture
test.cpp(13): error in "test_case1": check i == 1 has failed
teardown fixture
setup fixture
test.cpp(19): error in "test_case2": check i == 1 has failed [0 != 1]
teardown fixture
*** 2 failures are detected in test suite "example"
在此示例中,仅test_case1和test_case2分配了夹具F。 您仍然需要在每个测试用例中引用夹具名称。 本节说明如何在测试套件下为子树声明相同的夹具。
测试套件进/出夹具
全局夹具
自动注册
要声明不带参数的测试用例(已代替实现注册),请使用宏BOOST_AUTO_TEST_CASE。
BOOST_AUTO_TEST_CASE(test_case_name);
该API旨在紧密模拟无效的空函数声明语法。 与自由函数相比,您要做的就是跳过结果类型和方括号,并将测试用例名称包装到BOOST_AUTO_TEST_CASE中:
示例:基于空函数的自动注册测试用例
#define BOOST_TEST_MODULE example
#include <boost/test/included/unit_test.hpp>
BOOST_AUTO_TEST_CASE( free_test_function )
/* Compare with void free_test_function() */
{
BOOST_TEST( true /* test assertion */ );
}
输出:
example
Running 1 test case...
*** No errors detected
使用此宏,您无需执行任何其他注册步骤。 该宏会自动创建并注册一个名为free_test_function的测试用例。
手动注册
单元测试框架允许基于无参的空函数,无参的函数对象(包括使用boost :: bind和无参的boost :: function实例创建的对象)手动创建不带参数的测试用例。 为此,请使用宏BOOST_TEST_CASE:
BOOST_TEST_CASE(test_function);
BOOST_TEST_CASE创建一个boost::unit_test::test_case类的实例,并返回一个指向所构造实例的指针。 测试用例名称是从宏参数test_function推导出来的。 如果您希望分配其他测试用例名称,则必须
- 使用宏BOOST_TEST_CASE_NAME代替
- 或改用底层的make_test_case接口。
要注册一个新的测试用例,请使用方法test_suite::add。 测试用例的创建和注册都在测试模块初始化功能中执行。
这是手动注册的测试用例的最简单示例。 创建一个测试用例,并将其注册到测试模块初始化例程中。 请注意,无参函数名称是通过地址传递给宏BOOST_TEST_CASE的。
示例:手动注册的无空函数
#include <boost/test/included/unit_test.hpp>
using namespace boost::unit_test;
void free_test_function()
{
BOOST_TEST( true /* test assertion */ );
}
test_suite* init_unit_test_suite( int /*argc*/, char* /*argv*/[] ) {
framework::master_test_suite().
add( BOOST_TEST_CASE( &free_test_function ) );
framework::master_test_suite().
add( BOOST_TEST_CASE_NAME(&free_test_function, "second-check-free-test-function"));
return 0;
}
输出:
example --log_level=unit_scope
Running 2 test cases...
Entering test module "Master Test Suite"
example.cpp:20: Entering test case "free_test_function"
example.cpp:20: Leaving test case "free_test_function"; testing time: 50us
example.cpp:22: Entering test case "second-check-free-test-function"
example.cpp:22: Leaving test case "second-check-free-test-function"; testing time: 32us
Leaving test module "Master Test Suite"; testing time: 158us
*** No errors detected
测试用例可以实现为类的方法。 在这种情况下,必须将指向类实例的指针绑定到测试方法以创建测试用例。 您可以将类的相同实例用于多个测试用例。 单元测试框架不拥有类实例的所有权,因此您需要自己管理类实例的生命周期。
警告:不能在初始化函数范围内定义该类实例,因为一旦测试执行退出,该类实例就变得无效。 它需要静态/全局定义,或者使用共享指针进行管理。
示例:绑定到共享类实例并手动注册的类的空方法
#include <boost/test/included/unit_test.hpp>
#include <boost/bind.hpp>
using namespace boost::unit_test;
class test_class {
public:
void test_method1() {
BOOST_TEST( true /* test assertion */ );
}
void test_method2() {
BOOST_TEST( false /* test assertion */ );
}
};
test_suite* init_unit_test_suite( int /*argc*/, char* /*argv*/[] ) {
boost::shared_ptr<test_class> tester( new test_class );
framework::master_test_suite().
add( BOOST_TEST_CASE( boost::bind( &test_class::test_method1, tester )));
framework::master_test_suite().
add( BOOST_TEST_CASE( boost::bind( &test_class::test_method2, tester )));
return 0;
}
输出:
example
Running 2 test cases...
test.cpp(22): error: in "boost::bind( &test_class::test_method2, tester )": check false has failed
*** 1 failure is detected in the test module "Master Test Suite"
数据集
为了正确定义数据集,应该首先引入样本的概念。样本定义为多态元组。元组的大小将根据定义本身就是样本本身的大小。
数据集是样本的集合,
- 是向前迭代的
- 可以查询其大小,而大小又可以是无限的,
- 有一个参数,即它包含的样本的参数。
因此,数据集实现了序列的概念。
单元测试框架中数据集的描述能力来自
-
创建自定义数据集的接口非常简单,
-
他们提供的用于组合不同数据集的操作
-
它们与其他类型的集合(stl容器,C数组)的接口可用的内置数据集生成器
提示:仅支持“单态”数据集,这意味着单个数据集中的所有样本都具有相同的类型和相同的Arity [2]。但是,不同样本类型的数据集可以与zip和笛卡尔积组合在 一起。
正如我们将在下一节中看到的那样,代表不同类型集合的数据集可以组合在一起(例如zip或grid)。这些操作将产生新的数据集,其中的样本属于增强型。
数据集接口
数据集的接口应实现以下两个功能/字段:
- 迭代器begin(),其中迭代器是前向迭代器,
- boost::unit_test::data::size_t size() const表示数据集的大小。返回的类型是专用类size_t,它可以指示无限的数据集大小。
- 一个称为arity的枚举,指示数据集返回的样本的arity
声明数据集类D后,应通过专门化模板类将其注册到框架
boost::unit_test::data::monomorphic::is_dataset
条件是
boost::unit_test::data::monomorphic::is_dataset<D>::value
评估为true。
以下示例实现了生成斐波那契序列的自定义数据集。
示例:自定义数据集示例
#define BOOST_TEST_MODULE dataset_example68
#include <boost/test/included/unit_test.hpp>
#include <boost/test/data/test_case.hpp>
#include <boost/test/data/monomorphic.hpp>
#include <sstream>
namespace bdata = boost::unit_test::data;
// Dataset generating a Fibonacci sequence
class fibonacci_dataset {
public:
// the type of the samples is deduced
enum { arity = 1 };
struct iterator {
iterator() : a(1), b(1) {}
int operator*() const { return b; }
void operator++() {
a = a + b;
std::swap(a, b);
}
private:
int a;
int b; // b is the output
};
fibonacci_dataset() {}
// size is infinite
bdata::size_t size() const { return bdata::BOOST_TEST_DS_INFINITE_SIZE; }
// iterator
iterator begin() const { return iterator(); }
};
namespace boost { namespace unit_test { namespace data { namespace monomorphic {
// registering fibonacci_dataset as a proper dataset
template <>
struct is_dataset<fibonacci_dataset> : boost::mpl::true_ {};
}}}}
// Creating a test-driven dataset, the zip is for checking
BOOST_DATA_TEST_CASE(
test1,
fibonacci_dataset() ^ bdata::make( { 1, 2, 3, 5, 8, 13, 21, 35, 56 } ),
fib_sample, exp)
{
BOOST_TEST(fib_sample == exp);
}
输出:
example68
Running 9 test cases...
test.cpp(60): error: in "test1/7": check fib_sample == exp has failed [34 != 35]
_Failure occurred in a following context:
fib_sample = 34; exp = 35;
test.cpp(60): error: in "test1/_8": check fib_sample == exp has failed [55 != 56]
Failure occurred in a following context:
fib_sample = 55; exp = 56;
*** 2 failures are detected in the test module "dataset_example68"
数据集创建和延迟创建
上面定义的数据集是在测试模块甚至开始以全局对象开始执行之前构造的。 这使得无法从数据集生成器内部及其迭代期间访问argc / argv,主测试套件(和预处理的argc / argv)之类的元素,或在测试模块的主对象之后已实例化的任何其他对象 条目。
为了克服这个问题,引入了延迟的数据集实例化接口。 这样可以有效地将数据集包装在另一个数据集中,从而懒惰地实例化数据集。
要实例化延迟的数据集,应在BOOST_DATA_TEST_CASE调用中使用boost::unit_test::data::monomorphic::make_delayed函数。 以下代码段:
BOOST_DATA_TEST_CASE(dataset_test_case,
boost::unit_test::data::make_delayed<custom_dataset>(arg1, ... ), ...)
{
}
使用custom_dataset类型的生成器创建延迟的数据集测试用例。 生成器由arg1,...惰性构造。
提示:有关自定义命令行参数的部分提供了延迟创建的详细示例。
提示有关包装对象的更多详细信息,请参见类monomorphic :: delayed_dataset。
高级使用方案
head-only用法自定义
多个测试单元的header-only用法方式
即使测试模块具有多个测试单元文件(multiple translation units),也可以使用单元测试框架的head-only用法:
- 一个测试单元应定义
BOOST_TEST_MODULE
并包含<boost/test/included/unit_test.hpp>
头文件 - 所有其他的测试单元应包含
<boost/test/unit_test.hpp>
头文件
示例如下:
-
测试单元1,定义
BOOST_TEST_MODULE
#define BOOST_TEST_MODULE header-only multiunit test
#include <boost/test/included/unit_test.hpp>
BOOST_AUTO_TEST_CASE( test1 ) {
int i = 1;
BOOST_CHECK( i*i == 1 );
} -
测试单元2,包含
<boost/test/unit_test.hpp>
而不是<boost/test/included/unit_test.hpp>
头文件:#include <boost/test/unit_test.hpp>
BOOST_AUTO_TEST_CASE( test2 ) {
int i = 1;
BOOST_CHECK( i*i == 1 );
}
自定义模块的入口点
自定义模块的初始化函数
实际使用建议
教程
使用Boost.Test进行测试驱动的开发
今天是重要的一天 - 新年 的第一天。今天,我要开始新的生活。我将停止吃油腻的食物,开始参加健身俱乐部,并且……今天我将要测试我正在编写的程序。我可以在程序的最后一行完成后立即开始,或者更好的是,我可以在编码时编写测试。也许下次,我将在设计阶段的编码之前编写测试。我已经阅读了许多有关如何编写测试的文献,我手头有单元测试框架以及新类的想法。因此,让我们开始吧。
假设我想将一个长度不变的C字符缓冲区封装到简单类const_string中。基本原理:一个字符串类,它不分配内存,并且提供对预分配的字符缓冲区的方便的只读访问。我可能希望const_string具有类似于类std :: string的接口。我首先要做什么?在我的新生活中,我将从为将来的类const_string编写测试模块开始。它看起来像这样:
#define BOOST_TEST_MODULE const_string test
#include <boost/test/unit_test.hpp>
现在,我可以编译它并与单元测试框架链接。 做完了! 我有一个有效的测试程序。 它是空的,因此当我运行该程序时,它将产生以下输出:
*** No errors detected
好了,现在可能是开始在const_string上工作的好时机。 我想拥有的第一件事是构造函数和简单的访问方法。 因此,我的类初始版本如下所示:
class const_string {
public:
// Constructors
const_string();
const_string( std::string const& s )
const_string( char const* s );
const_string( char const* s, size_t length );
const_string( char const* begin, char const* end );
// Access methods
char const* data() const;
size_t length() const;
bool is_empty() const;
// ...
};
现在,我可以编写第一个测试用例构造函数测试并将其添加到测试套件中。 我的测试程序看起来像这样:
#define BOOST_TEST_MODULE const_string test
#include <boost/test/unit_test.hpp>
BOOST_AUTO_TEST_CASE( constructors_test ) {
const_string cs0( "" ); // 1 //
BOOST_TEST( cs0.length() == (size_t)0 );
BOOST_TEST( cs0.is_empty() );
const_string cs01( NULL ); // 2 //
BOOST_TEST( cs01.length() == (size_t)0 );
BOOST_TEST( cs01.is_empty() );
const_string cs1( "test_string" ); // 3 //
BOOST_TEST( std::strcmp( cs1.data(), "test_string" ) == 0 );
BOOST_TEST( cs1.length() == std::strlen("test_string") );
std::string s( "test_string" ); // 4 //
const_string cs2( s );
BOOST_TEST( std::strcmp( cs2.data(), "test_string" ) == 0 );
const_string cs3( cs1 ); // 5 //
BOOST_TEST( std::strcmp( cs3.data(), "test_string" ) == 0 );
const_string cs4( "test_string", 4 ); // 6 //
BOOST_TEST( std::strncmp( cs4.data(), "test", cs4.length() ) == 0 );
const_string cs5( s.data(), s.data() + s.length() ); // 7 //
BOOST_TEST( std::strncmp( cs5.data(), "test_string", cs5.length() ) == 0 );
const_string cs_array[] = { "str1", "str2" }; // 8 //
BOOST_TEST( cs_array[0] == "str1" );
BOOST_TEST( cs_array[1] == "str2" );
}
Constructors_test测试用例旨在检查const_string类的一个简单功能:一种根据不同参数正确构造自身的能力。 为了测试此功能,我将构造对象的这种特性用作其包含的数据和长度。 const_string类的规范不包含任何预期的失败,因此,尽管如果我将指针传递给无效的内存,构造函数可能会失败,但是不会执行错误检查控制(不需要要求未承诺的内容:-)) 。 但是对于任何有效的输入,它都应该起作用。 因此,我正在尝试检查一个空字符串(1),一个NULL字符串(2),一个常规C字符串(3),一个STL字符串(4),一个副本结构(5)等构造。 好了,修复了实现中的所有错误之后(您编写的程序是否从头开始没有错误吗?)我能够通过此测试用例,并且单元测试框架向我提供以下报告:
Running 1 test case...
*** No errors detected
鼓励我继续并添加更多访问方法:
class const_string {
public:
//...
char operator[]( size_t index ) const;
char at( size_t index ) const;
//...
};
我添加了新功能-我需要一个新的测试用例来检查它。 结果,我的测试套件看起来像这样:
#define BOOST_TEST_MODULE const_string test
#include <boost/test/unit_test.hpp>
BOOST_AUTO_TEST_CASE( constructors_test ) {
//...
}
BOOST_AUTO_TEST_CASE( data_access_test ) {
const_string cs1( "test_string" ); // 1 //
BOOST_TEST( cs1[(size_t)0] == 't' );
BOOST_TEST( cs1[(size_t)4] == '_' );
BOOST_TEST( cs1[cs1.length()-1] == 'g' );
BOOST_TEST( cs1[(size_t)0] == cs1.at( 0 ) ); // 2 //
BOOST_TEST( cs1[(size_t)2] == cs1.at( 5 ) );
BOOST_TEST( cs1.at( cs1.length() - 1 ) == 'g' );
BOOST_CHECK_THROW( cs1.at( cs1.length() ), std::out_of_range ); // 3 //
}
在data_access_test测试用例中,我试图检查const_string类字符访问的正确性。虽然测试(1)使用const_string::operator[]检查有效访问,而测试(2)使用方法const_string::at()检查有效访问,但还有另一件事要测试。方法const_string::at()的规范包含对越界访问的验证。测试(3)打算这样做:检查验证是否有效。验证和错误处理代码的测试是单元测试的重要组成部分,不应留给生产阶段。 data_access_test测试用例已通过,我已准备好进行下一步。
继续我的努力,我能够完成const_string类(请参见清单1 const_string.hpp)和测试模块(参见清单2 const_string_test.cpp),该模块将检查const_string规范中提供的所有功能。
好吧,我离实现我的新年愿望更近了(我们应该在下一个时候看到这个健身俱乐部……)。你呢?您的测试习惯可能有所不同。您可以从类/库开发开始,然后在某个时候开始基于功能编写测试用例。或者,在给定未来产品的详细规范(包括预期的接口)的情况下,您可以立即开始编写所有测试用例(也可以是其他人,而您同时从事实施工作)。无论如何,使用Boost.Test单元测试框架提供的功能都不会有任何问题,并且我希望能够编写稳定的防弹代码。而且更重要的是,您对更改任何复杂性的能力充满信心,而无需对整个产品进行冗长的回归测试。您的测试模块和单元测试框架将不为所动,以帮助您解决任何偶然的错误。
一个测试框架,用于什么?
测试程序应如何报告错误? 错误消息可能如下显示:
if( something_bad_detected )
std::cout << "something bad has been detected" << std::endl;