Doctest是一个为C++设计的轻量级单元测试和测试驱动开发(TDD)框架,它受到D语言的unittest模块和Python的docstrings的启发。Doctest的核心理念是测试可以被视为文档的一部分,应该能够与它们测试的生产代码相邻。这在其他任何C++测试框架中都是不可能的,或者至少是不切实际的。
Doctest的官方网站是,测试的版本是1.1.3。它支持C++98或更新的版本,采用MIT许可证,免费使用。支持通过GitHub项目页面的问题进行。
下面是一个完整的示例,展示了如何编写一个自注册的测试,该测试编译成一个可执行文件:
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest.h"
int fact(int n) {
return n <= 1 ? n : fact(n - 1) * n;
}
TEST_CASE("testing the factorial function") {
CHECK(fact(0) == 1);
// will fail
CHECK(fact(1) == 1);
CHECK(fact(2) == 2);
CHECK(fact(10) == 3628800);
}
该程序的输出如下: [doctest] doctest version is "1.1.3" [doctest] run with "--help" for options ======================================================== main.cpp(6) testing the factorial function main.cpp(7) FAILED! CHECK( fact(0) == 1 ) with expansion: CHECK( 0 == 1 ) ======================================================== [doctest] test cases: 1 | 0 passed | 1 failed [doctest] assertions: 4 | 3 passed | 1 failed 注意如何使用标准的C++等值比较运算符 - doctest有一个核心断言宏(它也有用于小于、等于、大于等的宏),但完整的表达式被分解,左右值被记录。这是通过表达式模板和C++技巧实现的。此外,测试用例是自动注册的 - 不需要手动将其插入到列表中。
Doctest是仿照Catch设计的,Catch是目前C++测试中最受欢迎的替代品 - 请查看中的差异。目前,Catch拥有的一些功能在doctest中还缺失,但doctest最终将成为Catch的超集。
Doctest与其他框架的不同之处在于,它在编译时间上非常轻(数量级上更轻),并且不侵入性。它与其他框架的主要区别在于:
所有上述优点使得该框架的用途比其他任何框架都要多 - 测试可以直接编写在生产代码中!这大大降低了编写测试的障碍 - 不必:
测试内部未通过公共API和头文件公开的部分变得更加容易。C++中的测试驱动开发从未如此简单!该框架仍然可以像其他任何框架一样使用,即使不喜欢在生产代码中编写测试的想法 - 但这是框架的最大优势 - 没有其他任何东西可以提供!
有许多其他特性,并且在路线图中计划了更多。如在上面的示例中看到的 - 程序的main()入口点可以由框架提供。然而,如果在生产代码中编写测试,可能已经有一个main()函数。以下代码示例展示了如何从用户main()使用doctest:
#define DOCTEST_CONFIG_IMPLEMENT
#include "doctest.h"
int main(int argc, char **argv) {
doctest::Context ctx;
ctx.setOption("abort-after", 5); // default - stop after 5 failed asserts
ctx.applyCommandLine(argc, argv); // apply command line - argc / argv
ctx.setOption("no-breaks", true); // override - don't break in the debugger
int res = ctx.run(); // run test cases unless with --no-run
if (ctx.shouldExit()) // query flags (and --exit) rely on this
return res; // propagate the result of the tests
// your code goes here
return res; // + your_program_res
}
通过这种设置,以下三种场景是可能的:
注意,DOCTEST_CONFIG_IMPLEMENT或DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN标识符应该在包含框架头文件之前定义 - 但仅在一个源文件中 - 测试运行器将在那里实现。在其他地方,只需包含头文件并编写一些测试。这是需要在单个源文件中编译的单头库的常见做法(在这种情况下,是测试运行器)。
可能希望在构建将交付给客户的发布版本时,从生产代码中移除测试。使用doctest实现这一点的方法是在整个项目中定义DOCTEST_CONFIG_DISABLE预处理器标识符。
例如,该标识符对TEST_CASE宏的影响如下 - 它变成了一个永远不会被实例化的匿名模板:
#define TEST_CASE(name) \
template
这意味着所有测试用例都从结果二进制文件中剔除 - 即使在调试模式下也是如此!链接器永远不会看到匿名测试用例函数,因为它们从未被实例化。
ANONYMOUS()宏用于每次调用时获得唯一的标识符 - 它使用__COUNTER__预处理器宏,该宏每次使用时返回一个比上一次大1的整数。例如:
int ANONYMOUS(ANON_VAR_);
// int ANON_VAR_5;
int ANONYMOUS(ANON_VAR_);
// int ANON_VAR_6;
子用例 - 在测试用例之间共享设置/拆除代码的最简单方法。假设想在几个测试用例中打开一个文件并从中读取。如果不想多次复制/粘贴相同的设置代码,可能会使用doctest的子用例机制。
TEST_CASE("testing file stuff") {
printf("opening the file\n");
FILE* fp = fopen("path/to/file", "r");
SUBCASE("seeking in file") {
printf("seeking\n");
// fseek()
}
SUBCASE("reading from file") {
printf("reading\n");
// fread()
}
printf("closing...\n");
fclose(fp);
}
将打印以下文本: opening the file seeking closing... opening the file reading closing... 如所见,测试用例被进入了两次 - 每次进入了一个不同的子用例。子用例也可以无限嵌套。执行模型类似于DFS遍历 - 每次从测试用例的开始处开始,遍历“树”直到达到一个尚未遍历过的叶节点(然后退出测试用例) - 然后通过弹出已进入的嵌套子用例的堆栈退出测试用例。
编译时间基准。因此,有三种类型的编译时间基准与doctest相关: