【问题标题】:Unit Testing a Function Macro对函数宏进行单元测试
【发布时间】:2017-12-16 18:06:55
【问题描述】:

我正在为一些函数宏编写单元测试,它们只是一些函数调用的包装器,并包含一些内部维护。

我整个上午都在写测试,我的大脑开始变得乏味,所以这可能只是一个隧道视觉的例子,但是:

对于宏扩展的单元测试是否有有效的案例?我的意思是检查是否为函数宏参数的各种源代码形式生成了正确的函数行为。例如,函数参数可以采用以下形式,在源代码中:

  • 字面意思
  • 变量
  • 运算符表达式
  • 结构成员访问
  • 指向结构成员的访问权限
  • 指针解引用
  • 数组索引
  • 函数调用
  • 宏展开
  • (请随时指出我遗漏的任何内容)

如果宏没有正确展开,那么代码通常甚至无法编译。那么,如果参数是float 字面量或float 变量,或者函数调用的结果,那么在不同的单元测试中是否还有任何明智之处?

扩展应该是单元测试的一部分吗?

【问题讨论】:

  • 当然,如果您忘记在宏参数的每个实例周围加上括号。否则,没有那么多。但实际上,这主要是一个意见问题,或者至少是一个个案决定。
  • 使用诸如value & 1 之类的表达式可能会表明宏是粗心的,但代码检查也可以做到这一点。我认为通过全套测试是矫枉过正的。乏味是给你一个相关的警告。
  • @JonathanLeffler:是的,有时我必须注意自己。我进入了覆盖边缘案例的凹槽并勾选了所有框,如果我不小心的话,我会最终编写测试来涵盖日食和火星入侵。

标签: c unit-testing macros


【解决方案1】:

正如我在comment 中指出的:

使用诸如value & 1 之类的表达式可能会表明宏很粗心,但代码检查也可以做到这一点。

我认为通过全套测试是矫枉过正的。乏味是给你一个相关的警告。

还有另一种可能相关的检查模式,即副作用,例如:x++ + ++y 作为参数。如果对宏的参数进行多次评估,则副作用可能会被打乱,或者至少会重复。作为参数的 I/O 函数(getchar()printf("Hello World\n"))也可能会暴露参数管理不善。

这也部分取决于您要应用于宏的规则。然而,如果它们看起来和行为都像函数调用,它们应该只计算一次参数(但它们应该计算每个参数——如果宏根本不计算参数,那么副作用就不会发生应该发生(如果宏真的是一个函数就会发生)。

另外,不要低估inline 函数的价值。

【讨论】:

    【解决方案2】:

    根据 cmets 和 @Jonathan Leffler 的回答中提出的一些观点,我得出的结论是,这是在功能测试中更好地测试的东西,最好是使用模糊测试器。

    这样,使用几个自动化脚本,fuzzer 可以在函数宏中抛出大量参数,并记录那些无法编译、产生编译器警告或编译并运行但产生不正确结果的参数。

    由于 fuzz 测试不应该快速运行(如单元测试),因此只需将其添加到 fuzz 套件并让它在周末运行就没有问题。

    【讨论】:

      【解决方案3】:

      测试的目的是发现错误。而且,您的宏定义可能包含错误。因此,一般都有测试宏的情况,特别是单元测试可以发现很多具体的错误,如下所述。

      显然也可以使用代码检查来发现错误,但是,两者兼而有之的好处是:只要修改相应的代码(例如,为了反应),就可以廉价地重复单元测试。

      代码检查不能廉价地重复(至少它们比重新运行单元测试需要更多的努力),但它们也可以找到测试无法检测到的其他点,例如错误或错误的文档、代码等设计问题复制等。

      也就是说,在对宏进行单元测试时,您会发现许多问题,其中一些问题已经提到过。而且,原则上可能会有模糊测试人员也检查此类问题,但我怀疑宏定义的问题是否已经成为模糊测试人员关注的焦点:

      • 错误的算法:宏定义中的表达式和语句可能与正常的非宏代码中的错误一样。
      • 括号不足(如 cmets 中所述):这是宏的潜在问题,即使在编译时,也可以通过将具有低优先级运算符的表达式作为宏参数传递来检测到它。例如,如果FOO(a) 被定义为(2 * a) 而不是(2 * (a)),则在测试代码中调用FOO(x = 2) 将导致编译错误。
      • 在扩展中意外多次使用参数(如 Jonathan 所述):这也是宏特有的潜在问题。它应该是宏规范的一部分,它的参数在扩展代码中被评估的频率(有时不能给出固定的数字,请参阅assert)。可以通过传递带有副作用的宏参数来测试有关多久评估一次参数的此类声明,然后可以通过测试代码进行检查。例如,如果 FOO(a) 定义为 ((a) * (a)),则调用 FOO(++x) 将导致 x 增加两次而不是一次。
      • 意外扩展:有时宏会以某种方式扩展而不会产生任何代码。 assertNDEBUG 是这里的一个例子,它应该扩展,以便扩展的代码将被完全优化掉。宏是否应该以这种方式扩展通常取决于配置宏。要检查相应配置的宏是否真正“消失”,可以使用语法错误的宏参数:例如,FOO(++ ++) 可以是编译时测试,以查看非空扩展之一是否代替空扩展使用过(但这是否有效,取决于非空扩展是否使用参数)。
      • 分号错误:为了确保像宏这样的函数干净地扩展为复合语句(使用适当的 do-while(0) 包装器,但没有尾随分号),可以使用像 if (0) FOO(42); else .. 这样的编译时检查。

      注意:我提到的那些测试是编译时测试,严格来说,只是某种形式的静态分析。与使用静态分析工具相比,此类测试有利于专门测试宏根据其设计预期具有的那些属性。同样,静态分析工具通常会在扩展中使用不带括号的宏参数时发出警告 - 但是,在许多扩展中,有意不使用括号。

      【讨论】:

        猜你喜欢
        • 2013-05-22
        • 2015-08-26
        • 2019-05-16
        • 2021-01-03
        • 2023-03-26
        • 2014-02-08
        • 1970-01-01
        • 1970-01-01
        • 2019-04-15
        相关资源
        最近更新 更多