深入理解Doctest:C++单元测试框架

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与其他框架的不同之处在于,它在编译时间上非常轻(数量级上更轻),并且不侵入性。它与其他框架的主要区别在于:

  • 超轻量级 - 将头文件包含在源文件中的编译时间开销低于10ms
  • 尽可能快的断言宏 - 50000个断言可以在30秒内编译完成(甚至在10秒内)
  • 子用例 - 一种直观的方式来共享测试用例的公共设置和拆除代码(替代固定装置)
  • 提供了一种从二进制文件中移除所有测试相关内容的方法,使用DOCTEST_CONFIG_DISABLE标识符
  • 不污染全局命名空间(所有内容都在doctest命名空间中)并且不拖动任何头文件
  • 即使在MSVC / GCC / Clang的最激进警告级别下,也不会产生任何警告 - Clang的-Weverything / MSVC的/W4 -Wall -Wextra -pedantic和其他50多个标志!
  • 非常便携且经过良好测试的C++98 - 每次提交都在CI上测试超过220种不同的构建,使用不同的编译器和配置(gcc 4.4-6.1 / clang 3.4-3.9 / MSVC 2008-2015,调试/发布,x86/x64,linux / windows / osx,valgrind,sanitizers...)
  • 只有一个头文件,除了C / C++标准库之外没有外部依赖(在测试运行器中使用)

所有上述优点使得该框架的用途比其他任何框架都要多 - 测试可以直接编写在生产代码中!这大大降低了编写测试的障碍 - 不必:

  • 制作一个单独的源文件
  • 在其中包含一堆东西
  • 将其添加到构建系统
  • 将其添加到源代码控制
可以直接在类或功能的生产代码的底部 - 甚至是头文件中编写测试!生产代码中的测试可以被视为文档或最新的注释 - 显示API的使用方法(由编译器强制正确性)。

测试内部未通过公共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 }

通过这种设置,以下三种场景是可能的:

  • 仅运行测试(使用--exit选项)
  • 仅运行用户代码(使用--no-run选项)
  • 运行测试和用户代码
如果打算直接在生产代码中编写测试,那么这些必须是可能的。此外,此示例还展示了如何为命令行选项设置默认值和覆盖值。

注意,DOCTEST_CONFIG_IMPLEMENT或DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN标识符应该在包含框架头文件之前定义 - 但仅在一个源文件中 - 测试运行器将在那里实现。在其他地方,只需包含头文件并编写一些测试。这是需要在单个源文件中编译的单头库的常见做法(在这种情况下,是测试运行器)。

可能希望在构建将交付给客户的发布版本时,从生产代码中移除测试。使用doctest实现这一点的方法是在整个项目中定义DOCTEST_CONFIG_DISABLE预处理器标识符。

例如,该标识符对TEST_CASE宏的影响如下 - 它变成了一个永远不会被实例化的匿名模板: #define TEST_CASE(name) \ template \ static inline void ANONYMOUS(ANON_FUNC_)() 这意味着所有测试用例都从结果二进制文件中剔除 - 即使在调试模式下也是如此!链接器永远不会看到匿名测试用例函数,因为它们从未被实例化。

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相关:

  • 包含头文件的成本
  • 断言宏的成本
  • 使用DOCTEST_CONFIG_DISABLE标识符移除所有测试时构建时间的下降
总结: 在上,可以看到设置和更多细节。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485