最近, zerocost发表了一篇有趣的文章, “没有宏和动态内存的C ++测试” ,其中讨论了用于测试C ++代码的简约框架。 作者(几乎)设法避免使用宏来注册测试,但代替它们的是,代码中出现了“魔术”模板,对我个人而言,对不起,这很难想象。 看完这篇文章后,我感到不满意,因为我知道可以做得更好。 我不立即记得在哪里,但是我肯定看到了测试代码,该代码不包含用于注册它们的单个额外字符:
void test_object_addition() { ensure_equals("2 + 2 = ?", 2 + 2, 4); }
最后,我记得该框架称为Cutter ,它使用一种天才方法以自己的方式识别测试功能。
(KDPV来自C BY网站SA的Cutter网站。)
诀窍是什么?
测试代码在单独的共享库中汇编。 测试函数是从导出的库符号中提取的,并通过名称进行标识。 测试由特殊的外部实用程序执行。 智人坐着。
$ cat test_addition.c #include <cutter.h> void test_addition() { cut_assert_equal_int(2 + 2, 5); }
$ cc -shared -o test_addition.so \ -I/usr/include/cutter -lcutter \ test_addition.c
$ cutter . F ========================================================================= Failure: test_addition <2 + 2 == 5> expected: <4> actual: <5> test_addition.c:5: void test_addition(): cut_assert_equal_int(2 + 2, 5, ) ========================================================================= Finished in 0.000943 seconds (total: 0.000615 seconds) 1 test(s), 0 assertion(s), 1 failure(s), 0 error(s), 0 pending(s), 0 omission(s), 0 notification(s) 0% passed
这是Cutter文档中的示例 。 您可以安全地滚动与Autotools相关的所有内容,而仅查看代码。 框架有点奇怪,是的,就像日语一样。
我不会过多地介绍实现功能。 我也没有完整(甚至至少是草稿)的代码,因为我个人并不真正需要它(在Rust中,一切都是开箱即用的)。 但是,对于有兴趣的人,这可能是一个很好的练习。
详细信息和实施选项
考虑在编写使用Cutter方法进行测试的框架时需要解决的一些任务。
获取导出功能
首先,您需要以某种方式使用测试功能。 当然,C ++标准根本没有描述共享库。 Windows最近已收购了Linux子系统,该子系统可将所有三个主要操作系统都简化为POSIX。 如您所知,POSIX系统提供了函数dlopen() , dlsym() , dlclose() ,借助它们,您可以获取功能的地址,知道其符号的名称,然后就可以了。 POSIX未公开所加载库中包含的功能列表。
不幸的是(尽管很幸运),没有标准的,可移植的方法来发现从库中导出的所有功能。 也许, 库概念并非在所有平台上都存在(阅读:嵌入式),这一事实在某种程度上涉及到这一点。 但这不是重点。 最主要的是您必须使用特定于平台的功能。
作为初始近似值,您可以简单地调用nm实用程序:
$ cat test.cpp void test_object_addition() { }
$ clang -shared test.cpp
$ nm -gj ./a.out __Z20test_object_additionv dyld_stub_binder
解析其输出并使用dlsym() 。
对于更深入的自省,像libelf , libMachO , pe-parse之类的库很有用,它允许您以编程方式解析您感兴趣的可执行文件和平台库。 实际上, nm和公司只是使用它们。
测试功能过滤
您可能已经注意到,这些库包含一些奇怪的字符:
__Z20test_object_additionv dyld_stub_binder
这就是__Z20test_object_additionv是什么,当我们仅test_object_addition函数test_object_addition ? 剩下的dyld_stub_binder什么?
“ __Z20... ”字符__Z20...是所谓的名称修饰 (名称修饰 )。 C ++编译功能,无法解决,请耐心等待。 从系统的角度来看,这就是所谓的函数(和dlsym() )。 为了将它们以正常形式显示给某个人,您可以使用libdemangle之类的库 。 当然,您需要的库取决于您使用的编译器,但是在平台框架内装饰格式通常是相同的。
至于像dyld_stub_binder这样的奇怪功能,这些也是必须考虑的平台功能。 在开始测试时,您不需要调用任何函数,因为那里没有鱼。
这种想法的逻辑延续是按名称过滤功能。 例如,您只能运行名称中带有test函数。 或者只是来自tests名称空间的功能。 并且还使用嵌套名称空间对测试进行分组。 您的想象力没有限制。
通过可执行测试的上下文
带有测试的目标文件收集在一个共享的库中,其代码的执行完全由外部实用程序驱动程序cutter的cuter控制。 因此,内部测试功能可以使用此功能。
例如,可执行测试的上下文(原始文章中的IRuntime )可以安全地通过全局(线程局部)变量传递。 驱动程序负责管理和传递上下文。
在这种情况下,测试函数不需要参数,但保留所有高级功能,例如,对测试用例进行任意命名:
void test_vector_add_element() { testing::description("vector size grows after push_back()"); }
description()函数通过全局变量访问条件IRuntime ,因此可以将注释传递IRuntime的框架。 框架保证使用全局上下文的安全性,而不是测试编写者的责任。
通过这种方法,将上下文传递给比较语句和内部测试函数时,可能需要从主函数中调用这些代码,从而减少了代码中的噪音。
构造函数和析构函数
由于测试的执行完全由驱动程序控制,因此它可以在测试周围执行其他代码。
Cutter库为此使用以下功能:
cut_setup() -每次单独测试之前cut_teardown() -每次单独测试后cut_startup() -运行所有测试之前cut_shutdown() -完成所有测试之后
仅在测试文件中定义了这些函数。 您可以在其中放置测试环境(夹具)的准备和清理:创建必要的临时文件,测试对象的复杂设置以及其他测试反模式。
对于C ++,可以提出一个更惯用的界面:
- 更面向对象和类型安全
- 具有更好的RAII概念支持
- 使用Lambda推迟执行
- 涉及测试执行上下文
但现在,我现在再次详细考虑这一点。
独立的测试可执行文件
为了方便起见,Cutter使用共享库方法。 各种测试被编译到一组库中,一个单独的测试实用程序可以找到并执行这些库。 当然,如果需要,可以将测试驱动程序的整个代码直接嵌入到可执行文件中,从而获得通常的单独文件。 但是,这将需要与构建系统进行协作,以便以正确的方式组织这些可执行文件的布局:无需删除“未使用”的功能,具有正确的依存关系等。
其他
Cutter和其他框架还具有许多其他有用的功能,可以简化编写测试时的工作:
- 灵活且可扩展的测试语句
- 从文件构建和获取测试数据
- 堆栈跟踪研究,异常和丢弃处理
- 可自定义的测试“细分级别”
- 在多个流程中运行测试
编写自行车时,值得回顾一下现有的框架。 UX是一个更深层次的话题。
结论
Cutter框架使用的方法允许以最小的程序员负担来识别测试功能,而只需编写测试功能即可。 该代码不需要使用任何特殊的模板或宏,从而提高了其可读性。
组装和运行测试的功能可以隐藏在用于Makefile,CMake等组装系统的可重用模块中。关于单独组装测试的问题仍然必须以另一种方式提出。
这种方法的缺点包括难以将测试放置在与主代码相同的文件(相同的转换单元)中。 不幸的是,在这种情况下,没有其他提示,就不再可能确定需要启动哪些功能,而哪些不需要。 幸运的是,在C ++中,通常习惯将测试和实现分发到不同的文件中。
至于宏的最终处理,在我看来, 原则上不应丢弃它们。 宏允许例如编写较短的比较语句,从而避免代码重复:
void test_object_addition() { ensure_equals(2 + 2, 5); }
但是在出现错误的情况下,同时保留该问题的相同信息内容:
Failure: test_object_addition <ensure_equals(2 + 2, 5)> expected: <5> actual: <4> test.c:5: test_object_addition()
可以从收集的库中包含的调试信息中提取理论上要测试的功能的名称,文件名以及该功能开始的行号。 ensure_equals()函数知道比较表达式的期望值和实际值。 宏允许您“恢复”测试语句的原始拼写,从中可以更清楚地看出为什么期望值4 。
但是,这并不适合所有人。 宏对测试代码的好处到此为止吗? 我还没有真正考虑过这一刻,这可能是进一步发展的一个好领域 变态 研究。 一个更有趣的问题:是否可以以某种方式为C ++创建没有宏的模拟框架 ?
细心的读者还指出,实施过程中实际上没有SMS和石棉,这对于地球的生态和经济而言无疑是有利的。