- 测试
- 开发工具
【免费下载链接】doctest
The fastest feature-rich C++11/14/17/20/23 single-header testing framework
doctest是一款 C++11/14/17/20/23 单头文件测试框架,即使完全不传任何命令行参数也能开箱即用地运行测试。但当测试规模增长、需要接入 CI、或要把测试框架嵌入生产代码时,本文整理的这套命令行体系——查询型参数(Query flags)、字符串/整型选项、布尔选项、通配符过滤器以及--dt-前缀互操作机制——就是精确控制测试行为的核心工具。读完本文你将掌握 doctest 全部命令行参数的语义、默认值与源码实现,并学会在自定义main()中用代码覆盖参数,以及如何与既有程序的命令行解析和平共处。
命令行参数总体设计
doctest 的命令行参数分为三大类,理解它们的语法差异是正确使用的第一步:
- 查询型参数(Query flags):打印结果后程序直接退出,不执行任何测试用例。若框架被集成进由客户端代码提供
main()的程序(见 doc/markdown/main.md),程序应在调用doctest::Context的run()之后检查shouldExit()的返回值并自行退出——是否退出由用户决定。 - 整型/字符串选项(Int/String options):必须在
=之后跟一个值,且等号两侧不能有空格,例如--order-by=rand。 - 布尔选项(Bool options):
=之后可跟1/yes/on/true或0/no/off/false;也可以像普通 flag 一样直接省略=value部分,此时默认视为true。
在 doctest/parts/private/context.cpp 的parseArgs()中可以看到这种"先解析布尔值、再回退为纯 flag"的实现逻辑:先尝试用parseIntOption()匹配--选项=值形式,若匹配失败再尝试用parseFlag()匹配裸 flag,命中即置为true;若两者都未命中且处于"带默认值"模式,则使用代码内默认值。真正的值解析由parseOptionImpl()完成,它从参数列表末尾向前扫描、取最后一个匹配项,并校验选项前的字符全部是-,避免误伤普通命令行参数。
查询型参数(Query flags)
| 查询参数 | 说明 |
|---|---|
-?--help-h | 打印帮助信息,列出全部参数与选项 |
-v--version | 打印 doctest 框架版本号 |
-c--count | 打印与当前过滤器匹配的测试用例数量 |
-ltc--list-test-cases | 按名称列出所有与当前过滤器匹配的测试用例 |
-lts--list-test-suites | 列出所有"至少有一个测试用例匹配当前过滤器"的测试套件 |
-lr--list-reporters | 列出所有已注册的 reporters |
从源码看,所有查询参数在命中后都会额外设置p->exit = true(见 doctest/parts/private/context.cpp),这正是shouldExit()返回true的依据。Context::run()中还会在进入测试循环前专门处理help、version、list_reporters与no_run分支:它们只向 reporters 广播report_query事件后便直接返回,绝不再执行任何测试用例。
整型/字符串选项
| 选项 | 说明 |
|---|---|
-tc--test-case=<filters> | 按测试用例名称过滤。默认所有测试用例都匹配;若传入如--test-case=*math*,*sound*,则只有至少命中逗号分隔列表中一个通配符模式的测试用例才会被执行/计数/列出 |
-tce--test-case-exclude=<filters> | 与--test-case=<filters>相反:逗号分隔列表中任一模式命中即跳过该测试用例 |
-sf--source-file=<filters> | 同--test-case=<filters>,但按测试用例所在源文件过滤 |
-sfe--source-file-exclude=<filters> | 同--test-case-exclude=<filters>,但按测试用例所在源文件过滤 |
-ts--test-suite=<filters> | 同--test-case=<filters>,但按测试套件过滤 |
-tse--test-suite-exclude=<filters> | 同--test-case-exclude=<filters>,但按测试套件过滤 |
-sc--subcase=<filters> | 同--test-case=<filters>,但按子用例(subcase)名称过滤。它不会过滤测试用例本身(测试用例必须执行才能发现子用例),所以建议与--test-case=<filters>搭配使用。注意:该选项不会运行选中子用例内部的嵌套子用例 |
-sce--subcase-exclude=<filters> | 同--test-case-exclude=<filters>,但按子用例名称过滤 |
-r--reporters=<filters> | 要使用的 reporters 列表,默认为console。可用逗号同时指定多个,如--reporters=xml,junit,执行顺序按注册优先级(数值越小越靠前) |
-o--out=<string> | 输出文件名。源码显示若指定的文件无法打开,会向std::cerr报错并回退到std::cout(doctest/parts/private/context.cpp) |
-ob--order-by=<string> | 执行前对测试用例排序,可选值:file(按所在文件)/suite(按测试套件)/name(按名称)/rand(随机)/none(不排序)。默认file。注意:file、suite、name三种排序结果与编译器相关,可能因编译器不同而不同 |
-rs--rand-seed=<int> | 随机排序的种子 |
-f--first=<int> | 通过当前过滤器的第一个要执行的测试用例——用于范围化执行,见 examples/range_based_execution.py |
-l--last=<int> | 通过当前过滤器的最后一个要执行的测试用例——用于范围化执行 |
-aa--abort-after=<int> | 累计失败断言数达到该值后停止执行测试用例/断言。默认 0 表示不停止。注意:框架用一个异常来终止当前测试用例,且与断言的级别(CHECK/REQUIRE)无关——所以请小心析构函数中的断言…… |
-scfl--subcase-filter-levels=<int> | 只对嵌套子用例的前<int>层应用子用例过滤器,更深层的子用例直接运行。默认值极大,相当于"过滤任何子用例" |
关于这些选项的默认值,源码(doctest/parts/private/context.cpp)给出了权威依据:out默认""、order-by默认"file"、rand-seed默认0、first默认0、last默认UINT_MAX、abort-after默认0、subcase-filter-levels默认INT_MAX。排序的具体实现在Context::run()中:file走fileOrderComparator(比较文件路径、行号与模板 id),suite走suiteOrderComparator(先套件后文件行号),name走nameOrderComparator(先名称后套件),rand则用std::srand(p->rand_seed)后执行一次随机洗牌,none保持注册顺序——这对"死亡测试"这类针对特定用例反复启动进程的场景尤其有价值,可以省去每次启动的排序开销。
布尔选项
| 选项 | 说明 |
|---|---|
-s--success=<bool> | 在输出中包含成功断言的详情 |
-cs--case-sensitive=<bool> | 过滤器是否区分大小写(默认不区分) |
-e--exit=<bool> | 测试结束后退出程序。仅当客户端提供了main()入口时才有意义——程序应在调用run()后检查shouldExit()并退出,以便"只跑测试、不继续业务逻辑" |
-d--duration=<bool> | 打印每个测试用例耗时(秒) |
-m--minimal=<bool> | 只打印失败的测试 |
-q--quiet=<bool> | 不打印任何输出。源码中该选项会让输出流指向一个DiscardOStream(doctest/parts/private/context.cpp) |
-nt--no-throw=<bool> | 跳过与异常相关的 断言 检查 |
-ne--no-exitcode=<bool> | 即使测试失败也总是返回成功退出码 |
-nr--no-run=<bool> | 跳过所有运行时 doctest 操作(但注册测试的代码发生在进入main()之前,不会受影响)。当框架集成进提供了main()的客户端代码、用户只想运行业务程序时非常有用 |
-ni--no-intro=<bool> | 输出中省略框架 intro |
-nv--no-version=<bool> | 输出中省略框架版本号 |
-nc--no-colors=<bool> | 禁用输出中的颜色 |
-fc--force-colors=<bool> | 即使检测不到 tty 也强制使用颜色 |
-nb--no-breaks=<bool> | 断言失败时禁止在调试器中打断点 |
-ns--no-skip=<bool> | 不跳过用装饰器标记为 skip 的测试用例(源码中对应tc.m_skip && !p->no_skip的判断,doctest/parts/private/context.cpp) |
-gfl--gnu-file-line=<bool> | 输出中的行号分隔符用:n:还是(n):(gnu 模式通常供 Linux 工具/IDE 使用,用:分隔)。源码显示其默认值为!DOCTEST_MSVC,即非 MSVC 平台默认开启 gnu 风格 |
-npf--no-path-filenames=<bool> | 打印文件名时去掉路径——希望不同环境得到相同输出时很有用 |
-sfp--strip-file-prefixes=<string> | 从输出中的任何文件路径里移除"前缀列表中最长匹配的那个前缀",与-npf类似,但通过保留部分相对路径来保留上下文。试一下:--sfp=${CMAKE_SOURCE_DIR}/:${CMAKE_BINARY_DIR}/。前缀列表的分隔字符见 doc/markdown/configuration.md#doctest_config_options_file_prefix_separator |
-nln--no-line-numbers=<bool> | 输出源位置时行号一律替换为0——当测试位置在源文件内变化时仍能得到一致输出 |
-ndo--no-debug-output=<bool> | 禁用附加调试器时输出到调试控制台 |
除了表格中的内容,源码还暴露了两个未出现在旧版表格中的布尔开关:--no-skipped-summary(nss,关闭被跳过用例的汇总)与--no-time-in-output(ntio,输出中不含时间),它们在较新版本中同样可用。
过滤器:通配符与转义规则
过滤器是一组逗号分隔的通配符模式:*表示"匹配任意序列",?表示"匹配任意单个字符"。
带空格的模式需要用双引号包裹,例如:
--test-case="*no sound*,vaguely named test number ?"包含逗号或反斜杠的模式可以用\转义,例如:
--test-case=this\,test\,has\,commas\,and\,a\\\,backslash\,followed\,by\,a\,comma如果反斜杠后面既不是\也不是,,则按原样保留,例如:
--test-case="Test that \ works correctly"注意:你的 shell 本身也可能用\做转义,因此\可能先被 shell 消费掉,而不是传给 doctest。
从实现层面看,doctest/parts/private/context.cpp 的parseCommaSepArgs()完整实现了这套转义规则:它逐字符扫描,遇到\后仅当下一字符是,或\时才输出该字符本身(转义),否则保留反斜杠;遇到未转义的,则切分出一个 filter 条目。过滤的最终应用则发生在Context::run()中:对每个测试用例依次核对 8 组过滤器(源文件包含/排除、套件包含/排除、名称包含/排除、子用例包含/排除),任何一组不匹配都会skip_me = true。源码里过滤器的存储布局是 9 个槽位的数组p->filters[0..8],其中filters[8]存放 reporters 列表。
前缀机制:--dt-与客户端命令行互操作
所有参数都有一个带前缀的版本,默认前缀为--dt-。例如--version也可以写作--dt-version或--dt-v。默认前缀可以通过定义DOCTEST_CONFIG_OPTIONS_PREFIX修改,见 doc/markdown/configuration.md#doctest_config_options_prefix。
同时,所有无前缀版本可以通过定义DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS整体禁用,见 doc/markdown/configuration.md#doctest_config_no_unprefixed_options。
这套设计的目的,是让框架被集成进客户端代码库时能轻松与其他命令行参数解析共存:所有 doctest 相关参数都可加上前缀以避免冲突,用户也能从自己的参数解析中排除一切以--dt-开头的参数。源码中parseOption()在DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS未定义时,会先用pattern + strlen(DOCTEST_CONFIG_OPTIONS_PREFIX)构造"去掉前缀的短模式"去匹配一遍(即无前缀形式),再匹配完整带前缀模式;定义该宏后则只匹配带前缀的形式。
用dt_removed辅助类过滤客户端参数
如果客户端没有"排除--dt-前缀参数"的现成机制,可以用框架提供的dt_removed辅助类把它们过滤掉:
#define DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS #define DOCTEST_CONFIG_IMPLEMENT #include "doctest.h" class dt_removed { std::vector<const char *> vec; public: dt_removed(const char **argv_in) { for (; *argv_in; ++argv_in) if (strncmp(*argv_in, "--dt-", strlen("--dt-")) != 0) vec.push_back(*argv_in); vec.push_back(NULL); } int argc() { return static_cast<int>(vec.size()) - 1; } const char **argv() { return &vec[0]; } // Note: non-const char **: }; int program(int argc, const char **argv); int main(int argc, const char **argv) { doctest::Context context(argc, argv); int test_result = context.run(); // run queries, or run tests unless --no-run if (context.shouldExit()) // honor query flags and --exit return test_result; dt_removed args(argv); int app_result = program(args.argc(), args.argv()); return test_result + app_result; // combine the 2 results } int program(int argc, const char **argv) { printf("Program: %d arguments received:\n", argc - 1); while (*++argv) printf("'%s'\n", *argv); return EXIT_SUCCESS; }当程序这样运行时:
program.exe --dt-test-case=math* --my-option -s --dt-no-breaks输出为:
Program: 2 arguments received: '--my-option' '-s'可以看到--dt-test-case=math*与--dt-no-breaks已被 doctest 消费并从传给客户端program()的参数中剔除,而-s由于被DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS禁用无前缀形式后不再被 doctest 解析,原样交给了客户端程序——这正是该示例演示"无前缀选项被禁用后交由客户端处理"的精妙之处。
在代码中设置选项:Context API 的 defaults/overrides
所有命令行选项都可以在代码中设置默认值或覆盖值——前提是你自己提供main()(详见 doc/markdown/main.md)。注意:查询 flag 无法用代码设置;过滤器只能通过addFilter()追加、或通过clearFilters()全部清空,用户无法用代码单独移除某个过滤器。
#define DOCTEST_CONFIG_IMPLEMENT #include "doctest.h" int main(int argc, char **argv) { doctest::Context context; // !!! THIS IS JUST AN EXAMPLE SHOWING HOW DEFAULTS/OVERRIDES ARE SET !!! // defaults context.addFilter("test-case-exclude", "*math*"); // exclude test cases with "math" in their name context.setOption("abort-after", 5); // stop test execution after 5 failed assertions context.setOption("order-by", "name"); // sort the test cases by their name context.applyCommandLine(argc, argv); // overrides context.setOption("no-breaks", true); // don't break in the debugger when assertions fail int res = context.run(); // run if (context.shouldExit()) // important - query flags (and --exit) rely on the user doing this return res; // propagate the result of the tests int client_stuff_return_code = 0; // your program - if the testing framework is integrated in your production code return res + client_stuff_return_code; // the result from doctest is propagated here as well }务必调用context.shouldExit()——当使用了查询 flag(或--no-run置为true)时它会被置位,由用户负责以正常方式退出程序。从源码看,Context构造函数与applyCommandLine()都会调用parseArgs()(前者额外传入withDefaults=true),而setOption()的实现是构造"-" + option + "=" + value形式的参数串再走一遍解析(doctest/parts/private/context.cpp),所以"先applyCommandLine再setOption"的调用顺序天然形成"命令行优先、代码后覆盖"的语义;若想在命令行之前设定默认值,则在applyCommandLine()之前调用即可。
实战场景:范围化执行(range-based execution)
--first=<int>与--last=<int>组合可以只执行通过当前过滤器后落入某个序号区间内的测试用例,这是把一个大测试集切分到多台机器或多个进程并行执行的利器。仓库附带的 examples/range_based_execution.py 演示了完整流程:
- 先用
--dt-count=1查询通过过滤器的未跳过用例总数(解析输出行[doctest] unskipped test cases passing the current filters:); - 按 CPU 核心数把
[1, N]的序号区间均分,例如 8 核跑 100 个用例时切分为([1, 13], [14, 26], [27, 39], [40, 52], [53, 65], [66, 78], [79, 91], [92, 100]); - 每个 worker 用
--dt-first=<first> --dt-last=<last>启动一个测试进程实例,通过multiprocessing.Pool并行执行。
该脚本配合--dt-前缀使用,说明范围化执行通常与前缀机制一起出现在需要脚本化、CI 化的场景中。仓库的测试输出样例(如 examples/all_features/test_output/count.txt)也印证了--count的输出格式,可作为对照基准。
与 reporters 的联动
--reporters=<filters>与-r是唯一以"过滤器"形式出现的运行时选项,选择结果决定了输出格式:默认console,另有流式(streaming)的xml和缓冲式(buffering)的junit可用,多个 reporter 用逗号分隔。所有已注册的 reporter/listener 可用--list-reporters查看;自定义 reporter 通过REGISTER_REPORTER注册后即可在命令行中被选中,详见 doc/markdown/reporters.md。输出目标默认是stdout,可用--out=<filename>重定向到文件。在 doctest/parts/private/context.cpp 中可以看到:若filters[8]为空则默认塞入"console",随后遍历所有已注册 reporters,用matchesAny()按过滤器匹配选择,选中的 reporter 实例在整个运行期间被缓存使用。
小结
doctest 的命令行体系可以概括为三条主线:查询 flag 控制"程序要不要跑测试"(--help/--version/--count/--list-*),过滤与排序选项控制"跑哪些测试、按什么顺序跑"(--test-case系列、--source-file系列、--test-suite系列、--subcase系列、--order-by、--first/--last),布尔选项控制"输出长什么样、失败后怎么处理"(--success、--minimal、--quiet、--abort-after、--no-*系列)。配合--dt-前缀与DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS/DOCTEST_CONFIG_OPTIONS_PREFIX两个配置宏,以及在自定义main()中通过Context::addFilter()/setOption()/applyCommandLine()的 defaults/overrides 组合,无论是独立测试程序、CI 并行执行还是嵌入生产代码,都能获得精确可控、且不与宿主程序参数冲突的测试运行体验。
更多相关参考:
- 自定义
main()与 DLL 场景:doc/markdown/main.md - 前缀与相关配置宏的完整定义:doc/markdown/configuration.md
- reporter 体系与自定义 reporter:doc/markdown/reporters.md
- 命令行解析核心实现:doctest/parts/private/context.cpp
- 范围化执行脚本:examples/range_based_execution.py
- 测试
- 开发工具
【免费下载链接】doctest
The fastest feature-rich C++11/14/17/20/23 single-header testing framework
相关推荐
doctest命令行工具完全指南:掌握所有选项和过滤器的使用技巧
doctest命令行工具完全指南:掌握所有选项和过滤器的使用技巧 doctest 是一个轻量级的C++测试框架,它不仅编译速度快,而且提供了丰富的命令行选项和过
测试开发工具Keystone GraphQL 查询过滤器(Filters)完全指南:从标量过滤到关系过滤
Keystone GraphQL 查询过滤器(Filters)完全指南:从标量过滤到关系过滤 Keystone 为 Node.js 开发者提供了强大的 Grap
后端InfluxDB 3.0 CLI 完全指南:从数据库创建到数据查询的命令行操作
InfluxDB 3.0 CLI 完全指南:从数据库创建到数据查询的命令行操作 你是否还在为时序数据管理工具的复杂操作而困扰?是否想快速掌握从数据库创建到数据查
数据库时序数据库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考