【问题标题】:How to test a static function如何测试静态函数
【发布时间】:2010-10-10 06:06:59
【问题描述】:

在对一些 C 代码应用单元测试时,我们遇到了一个问题,即在不修改源代码的情况下,无法在测试文件中调用某些静态函数。有没有什么简单合理的方法可以解决这个问题?

【问题讨论】:

标签: c unit-testing


【解决方案1】:

我有一个测试工具。在可怕的情况下——比如尝试测试静态函数,我使用:

#include "code_under_test.c"
...test framework...

也就是说,我在测试工具中包含了包含被测函数的整个文件。这是最后的手段 - 但它确实有效。

【讨论】:

  • 我不明白这一点。谁能解释一下。谢谢!
  • @BhupeshPant:你不明白什么?如果函数是static,则在定义它的翻译单元(大致是源文件)之外无法访问它。答案中的解决方案通过将要测试的源代码复制到通过#include "code_under_test.c" 指令对其进行测试的文件中,确保要测试的函数包含在测试它的代码中。编译器将要测试的代码和执行测试的代码视为一个翻译单元,因此测试代码可以调用静态函数,否则这是不可能的。
  • 我明白了!感谢你的及时回复!为什么我们不能根据条件编译类型添加包装函数。
  • 我刚刚看到@paxdiablo 也在说同样的话。
  • code_under_test.c 包含静态和非静态函数并与我的测试文件链接在一起时,这会给我带来问题。然后定义了两次非静态函数。
【解决方案2】:

您能否提供更多关于为什么不能调用该函数的信息?

因为它对 .c 文件是私有的,所以它不可用吗?如果是这样,最好的办法是使用允许访问该函数的条件编译,以允许其他编译单元访问它。例如

SomeHeaderSomewher.h

#if UNIT_TEST
#define unit_static 
#else
#define unit_static static
#endif

Foo.h

#if UNIT_TEST
void some_method
#endif

Foo.cpp

unit_static void some_method() ...

【讨论】:

  • 我喜欢这个破解。唯一的权衡是在生产代码中留下一些奇怪的标记,如 unit_static
【解决方案3】:

对于单元测试,我们实际上在源文件本身中有测试代码,我们在测试时有条件地编译它。这使单元测试可以完全访问所有函数和文件级变量(静态或其他)。

单元测试本身不是静态的 - 这允许我们从单个超级测试程序调用单元测试,该程序对所有编译单元进行单元测试。

当我们交付代码时,我们有条件地编译出单元测试,但这实际上并不是必需的(如果您想确定您交付的代码完全与您测试的代码相同)。

我们一直认为将单元测试与您正在测试的代码放在同一个地方是非常宝贵的,因为这让您在代码更改时需要更新测试变得更加明显。

【讨论】:

  • 当单元测试足够小以适应时,我会这样做。当单元测试变得比被测源更大时,我求助于单独的测试程序,它可以与对象链接(仅使用外部依赖项时)或在使用静态函数时使用我的答案中的技巧。
【解决方案4】:

不——你不能在不修改源代码的情况下直接测试静态函数(这是 C 中静态的定义——不能从不同文件中的函数调用它)。

您可以在测试文件中创建一个单独的函数来调用静态函数吗?

例如:

//Your fn to test
static int foo(int bar)
{
  int retVal;
  //do something
  return retVal;
}

//Wrapper fn
int test_foo(int bar)
{
  return foo(bar);
}

我们通常不直接测试我们的静态函数,而是确保它们执行的逻辑被调用函数的不同测试充分测试。

【讨论】:

  • 如果你不直接测试函数,那并不是真正的单元测试。
【解决方案5】:

静态函数本质上是公共(即公开)函数的辅助函数。因此,IMO,您的单元测试应该调用公共接口,并使用在静态函数中执行所有路径的输入。

应该使用公共函数的输出(返回值/副作用)来测试静态的效果。

这意味着您需要有适当的存根来“捕捉”这些副作用。 (例如,如果一个函数调用文件 IO,您需要提供存根来覆盖这些文件 IO 库函数)。通过使每个测试套件成为单独的项目/可执行文件并避免链接到任何外部库函数来做到这一点的最佳方法。你甚至可以模拟 C 函数,但这不值得。

无论如何,这是我迄今为止使用的方法,它对我有用。祝你好运

【讨论】:

  • 然而,自下而上测试的全部意义在于您不需要需要“调用在静态函数中执行所有路径的公共接口”。否则,就像在测试汽车时,您还要检查每个螺母和螺栓是否符合其规格。最好单独测试螺母和螺栓的生产,如果可以,您只需检查汽车驱动是否正确。不确定我是否解释得很好......
  • @Razzie,我明白你的意思。我想这是你画抽象线的地方。对我来说,通过单元测试,我将螺母检查视为单元测试,将整车视为集成测试。在这种情况下,静态函数将是螺母的材料成分。测试静态函数需要使用侵入式探针来污染源代码(例如在螺母上钻孔)
  • 有趣的想法,但我认为没有必要将静态函数视为“不是软件,而是材料”。 C 中的静态函数(和宏)是最简单的“单元”——就在“自下而上测试”的底部。在最简单的情况下,它们是最容易进行“单元测试”的:传递一组输入参数并检查输出参数和任何返回值。理想情况下,这些测试应与代码本身接近,例如您在商店购买的设备上的“自测”按钮。
【解决方案6】:

如果您在 Unix 环境下,您可以在测试文件中包含附加标头 yourheader_static.h 以及静态函数的声明,并将 obj 文件 code_under_test.o 转换为 objdump --globalize-symbols=syms_name_file 以全球化本地符号。它们将像非静态函数一样可见。

【讨论】:

    【解决方案7】:
    #define static
    

    这是一个非常糟糕的主意。如果您将变量声明为函数的局部变量,它会更改函数的行为。示例:

    static int func(int data)
    {
       static int count = 0;
    
       count += data;
       return count;
    }
    

    您可以从单元测试中调用该函数,因为 func() 将被导出,但代码的基本功能将被修改。

    --库尔特

    【讨论】:

      【解决方案8】:

      只是为了补充 Jonathan Leffler 接受的答案,并详细说明其他人提到的包装函数:

      1. 创建一个测试源文件,如 test_wrapper_foo.c,其中 foo.c 是原始文件。
      2. 在 test_wrapper_foo.c 中:
      #include "foo.c"   
      
      // Prototype
      int test_wrapper_foo();
      
      // wrapper function
      int test_wrapper_foo() {
          // static function to test
          return foo();
      }
      

      假设 foo.c 中的静态函数 foo 有原型:int foo(void);

      1. 通过您的 makefile 而不是 foo.c 构建 test_wrapper_foo.c(请注意,这不会破坏其他外部函数对 foo.c 中函数的任何依赖)

      2. 在您的单元测试脚本中,调用 test_wrapper_foo() 而不是 foo()。

      这种方法使原始源保持不变,同时让您可以从测试框架访问函数。

      【讨论】:

      • 但是,您将通过这种方式编译两次foo.c。一次是原始的,另一次是包含在包装器中的。
      【解决方案9】:

      你可以添加一个非静态函数来调用静态函数,然后调用非静态函数。

      static int foo ()
      {
         return 3;
      }
      
      #ifdef UNIT_TEST
      int test_foo ()
      {
        if (foo () == 3)
          return 0;
      
        return 1;
      }
      #endif
      

      【讨论】:

      • 那会修改源代码,不是吗?
      • 是的,它会修改源代码,但不会从 foo 中删除“静态”。否则,答案几乎是您不能从外部调用静态。我冒昧了。
      【解决方案10】:

      如果您使用 Ceedling 并尝试使用 #include "code_under_test.c" 方法,则测试构建将失败,因为它会在 #included 时自动尝试构建一次 "code_under_test.c" 并且因为它是目标测试。

      我已经能够通过对 code_under_test.c 代码的轻微修改和其他一些更改来解决它。用这个检查包裹整个 code_under_test.c 文件:

      #if defined(BUILD)
      ...
      #endif // defined(BUILD)
      

      将此添加到您的测试工具中:

      #define BUILD
      #include "code_under_test.c"
      

      BUILD 定义添加到您的 Makefile 或项目配置文件中:

      # Makefile example
      ..
      CFLAGS += -DBUILD
      ..
      

      您的文件现在将从您的环境构建,并包含在您的测试工具中。 Ceedling 现在将无法再次构建文件(确保您的 project.yml 文件未定义 BUILD)。

      【讨论】:

        【解决方案11】:

        以上所有建议的答案(少数除外)都建议进行条件编译,需要修改源代码。因此这不应该是一个问题,它只会增加混乱(仅用于测试)。相反,您可以这样做。

        说你要测试的功能是

        static int foo(int);
        

        您创建另一个名为 testing_headers.h 的头文件,其中包含内容 -

        static int foo(int);
        int foo_wrapper(int a) {
            return foo(a);
        }
        

        现在在编译你的 c 文件进行测试时,你可以从编译器选项中强制包含这个头文件。

        对于 clang 和 gcc,标志是 --include。对于 Microsoft C 编译器,它是 /FI

        这将需要对您的 c 文件进行绝对 0 更改,并且您将能够为您的函数编写一个非静态包装器。

        如果你不想要一个非静态包装器,你也可以创建一个初始化为 foo 的非静态全局函数指针。

        然后您可以使用这个全局函数指针调用该函数。

        【讨论】:

          【解决方案12】:

          有两种方法可以做到这一点。

          1. 将c源文件包含到单元测试源文件中,因此静态方法现在在单元测试源文件的范围内并且可以调用。

          2. 做个花招:

          #define static

          在单元测试源文件的头部。

          它将关键字static 转换为“无”,或者我可以说,它会从您的c 源代码中删除static

          在一些单元测试工具中,我们可以使用配置选项“预处理器”来做这个技巧,只需放入配置:

          static=

          该工具会将所有 static 关键字转换为“无”

          但我必须说,使用这些方式使static 方法(或变量)失去了它的特定含义。

          【讨论】:

          • 突然暴露大量在文件范围内声明为静态的函数和变量可能会导致不应发生的冲突。更严重的是,突然将在函数范围(函数内部)声明为静态的变量转换为非静态通常会完全破坏函数。因此,#define static /* as nothing */ 技巧并不可靠。
          • 是的,你是对的。这只是一个技巧,以防我们考虑了我们需要测试的内容,只是静态函数的内容或与之相关的所有事情。
          • 有趣的是,#define static /*as nothing*/ 正是 cmocka(从 Google 的 cmockery 演变而来的单元测试框架)所推荐的(这让我重新考虑使用它)
          猜你喜欢
          • 1970-01-01
          • 2015-11-12
          • 1970-01-01
          • 1970-01-01
          • 2013-04-09
          • 2018-09-28
          • 1970-01-01
          • 1970-01-01
          • 2018-08-29
          相关资源
          最近更新 更多