【发布时间】:2010-10-10 06:06:59
【问题描述】:
在对一些 C 代码应用单元测试时,我们遇到了一个问题,即在不修改源代码的情况下,无法在测试文件中调用某些静态函数。有没有什么简单合理的方法可以解决这个问题?
【问题讨论】:
-
绝对不是 C++ 或其他面向对象语言的问题的重复,因为 C 没有类、私有方法、字段或内部类。
标签: c unit-testing
在对一些 C 代码应用单元测试时,我们遇到了一个问题,即在不修改源代码的情况下,无法在测试文件中调用某些静态函数。有没有什么简单合理的方法可以解决这个问题?
【问题讨论】:
标签: c unit-testing
我有一个测试工具。在可怕的情况下——比如尝试测试静态函数,我使用:
#include "code_under_test.c"
...test framework...
也就是说,我在测试工具中包含了包含被测函数的整个文件。这是最后的手段 - 但它确实有效。
【讨论】:
static,则在定义它的翻译单元(大致是源文件)之外无法访问它。答案中的解决方案通过将要测试的源代码复制到通过#include "code_under_test.c" 指令对其进行测试的文件中,确保要测试的函数包含在测试它的代码中。编译器将要测试的代码和执行测试的代码视为一个翻译单元,因此测试代码可以调用静态函数,否则这是不可能的。
code_under_test.c 包含静态和非静态函数并与我的测试文件链接在一起时,这会给我带来问题。然后定义了两次非静态函数。
您能否提供更多关于为什么不能调用该函数的信息?
因为它对 .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。
对于单元测试,我们实际上在源文件本身中有测试代码,我们在测试时有条件地编译它。这使单元测试可以完全访问所有函数和文件级变量(静态或其他)。
单元测试本身不是静态的 - 这允许我们从单个超级测试程序调用单元测试,该程序对所有编译单元进行单元测试。
当我们交付代码时,我们有条件地编译出单元测试,但这实际上并不是必需的(如果您想确定您交付的代码完全与您测试的代码相同)。
我们一直认为将单元测试与您正在测试的代码放在同一个地方是非常宝贵的,因为这让您在代码更改时需要更新测试变得更加明显。
【讨论】:
不——你不能在不修改源代码的情况下直接测试静态函数(这是 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);
}
我们通常不直接测试我们的静态函数,而是确保它们执行的逻辑被调用函数的不同测试充分测试。
【讨论】:
静态函数本质上是公共(即公开)函数的辅助函数。因此,IMO,您的单元测试应该调用公共接口,并使用在静态函数中执行所有路径的输入。
应该使用公共函数的输出(返回值/副作用)来测试静态的效果。
这意味着您需要有适当的存根来“捕捉”这些副作用。 (例如,如果一个函数调用文件 IO,您需要提供存根来覆盖这些文件 IO 库函数)。通过使每个测试套件成为单独的项目/可执行文件并避免链接到任何外部库函数来做到这一点的最佳方法。你甚至可以模拟 C 函数,但这不值得。
无论如何,这是我迄今为止使用的方法,它对我有用。祝你好运
【讨论】:
如果您在 Unix 环境下,您可以在测试文件中包含附加标头 yourheader_static.h 以及静态函数的声明,并将 obj 文件 code_under_test.o 转换为 objdump --globalize-symbols=syms_name_file 以全球化本地符号。它们将像非静态函数一样可见。
【讨论】:
#define static
这是一个非常糟糕的主意。如果您将变量声明为函数的局部变量,它会更改函数的行为。示例:
static int func(int data)
{
static int count = 0;
count += data;
return count;
}
您可以从单元测试中调用该函数,因为 func() 将被导出,但代码的基本功能将被修改。
--库尔特
【讨论】:
只是为了补充 Jonathan Leffler 接受的答案,并详细说明其他人提到的包装函数:
#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);
通过您的 makefile 而不是 foo.c 构建 test_wrapper_foo.c(请注意,这不会破坏其他外部函数对 foo.c 中函数的任何依赖)
在您的单元测试脚本中,调用 test_wrapper_foo() 而不是 foo()。
这种方法使原始源保持不变,同时让您可以从测试框架访问函数。
【讨论】:
foo.c。一次是原始的,另一次是包含在包装器中的。
你可以添加一个非静态函数来调用静态函数,然后调用非静态函数。
static int foo ()
{
return 3;
}
#ifdef UNIT_TEST
int test_foo ()
{
if (foo () == 3)
return 0;
return 1;
}
#endif
【讨论】:
如果您使用 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)。
【讨论】:
以上所有建议的答案(少数除外)都建议进行条件编译,需要修改源代码。因此这不应该是一个问题,它只会增加混乱(仅用于测试)。相反,您可以这样做。
说你要测试的功能是
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 的非静态全局函数指针。
然后您可以使用这个全局函数指针调用该函数。
【讨论】:
有两种方法可以做到这一点。
将c源文件包含到单元测试源文件中,因此静态方法现在在单元测试源文件的范围内并且可以调用。
做个花招:
#define static
在单元测试源文件的头部。
它将关键字static 转换为“无”,或者我可以说,它会从您的c 源代码中删除static。
在一些单元测试工具中,我们可以使用配置选项“预处理器”来做这个技巧,只需放入配置:
static=
该工具会将所有 static 关键字转换为“无”
但我必须说,使用这些方式使static 方法(或变量)失去了它的特定含义。
【讨论】:
#define static /* as nothing */ 技巧并不可靠。
#define static /*as nothing*/ 正是 cmocka(从 Google 的 cmockery 演变而来的单元测试框架)所推荐的(这让我重新考虑使用它)