低成本自动化单元测试:内联变量与模块,哪个才是最优解?

发布时间:2026/7/7 4:24:47

低成本自动化单元测试:内联变量与模块,哪个才是最优解? 低成本自动化单元测试第二部分——C 代码封装在这个分为两部分的系列文章的第一部分中我介绍了一种经过时间检验也就是有点老派啦的技术它以极其简单的方式处理自动化单元测试包括验证异常处理是否正确。不过上一篇文章留下了两个问题而解决这些问题的过程恰好让我们领略了现代 C 的两个关键特性内联变量和模块。第一部分讨论的测试框架之所以简单是因为所有内容都包含在一个小的头文件 test.h 中未显示包含保护。一般来说用户只需调用 test_ 宏它会将被测试的表达式以文本形式捕获并记录相关的文件名和行号。如果测试失败会在控制台输出相应内容。report_ 函数会输出测试成功和失败的数量其他函数是为了完整性而存在但用户很少需要用到。在本文中我将解决第一部分结尾提出的两个问题一是匿名命名空间中的计数器是每个文件独有的这是不必要的限制大型项目应该在所有项目文件中共享成功和失败的总数同时不违反单一定义规则ODR二是 C 中对头文件的依赖一直是个令人头疼的问题C20 引入了模块来缓解这类问题。事实证明这里仍然需要宏所以我将采用一种混合方法尽可能将代码迁移到模块中。内联变量C17 引入了 inline 变量的概念。和函数一样inline 变量可以在多个翻译单元中定义链接器会将所有这些定义合并为一个。其规则与内联函数类似。这里的解决方法是选择一个命名命名空间并将 nPass 和 nFail 声明为 inline。由于所有函数都在 TestFramework 命名空间中我将 do_fail 重命名为 faildo_test 重命名为 test还去掉了后两个函数名末尾的下划线只有宏保留了末尾的下划线宏还需要对相关函数进行全限定调用。这些变量满足 ODR并且不会污染全局命名空间。如果你想继续使用仅头文件的方式这是一个不错的解决方案。这很简单。模块迁移模块是一种更简洁、更现代的功能封装方式。事实上既然 C 有了模块现在对内联变量的需求就不大了。变量和函数现在可以移到它们自己的模块中。在这样做之前我们需要确定是否仍然需要宏以及它们将如何与模块交互。最初使用宏的主要动机是将被测试的表达式捕获为字符串据我所知没有其他替代方法所以 test_、fail_、throw_ 和 nothrow_ 这些宏仍然需要保留。不过为了捕获文件名和行号我将使用 C20 引入的 std::source_location。宏是预处理器的产物不能从模块中导出。这里的解决方案是将宏放在一个单独的头文件中并让它们调用 TestFramework 命名空间。按照当前的惯例我将把变量和函数移到一个名为 test 的模块中该模块位于 test.cppm 文件中。请注意TestFramework 命名空间也必须显式导出。由于所有内容都在自己的模块中现在不再需要 inline 关键字了。但是要使用这个模块必须在包含 test_macros.h 头文件之前导入 test 模块这样宏才能看到 TestFramework 命名空间。以下是第一部分中的 Stack 测试示例重写后使用了 test 模块。客户端代码的唯一区别是使用了 import 语句并在导入模块后包含了头文件。实际应该选择哪种方法总结一下两个版本都解决了本系列文章开头提到的 ODR 问题。内联变量的解决方案只需要多一个关键字并且在 -stdc17 支持的所有地方都能工作不需要模块依赖扫描器不用担心二进制模块接口BMI也不用在作业截止前的晚上 11 点向学生解释编译顺序的问题。如果你有 C20 可用还可以用 std::source_location 替换 __FILE__ 和 __LINE__仍然使用仅头文件的方式。模块版本提供了真正的封装不仅仅是一个礼貌的命名空间并且避免了在每个翻译单元中重新解析头文件但它要求你处理两个文件并确保 import 语句在 #include 之前而且在 Visual C 之外的工具链中模块支持还不够完善。我的建议是对于课程作业以及任何需要在任何环境中立即构建的项目默认使用仅头文件的版本。当你的构建环境真正具备了强大的模块支持并且你确实需要更强的边界时再考虑使用模块版本——在生态系统跟上语言发展之前将其视为目标而不是日常使用的方式。模块和头文件混合的方法在很长一段时间内可能都会存在。

相关新闻