如何在普通C中编写unit testing?

我已经开始深入研究GLib文档并发现它还提供了一个unit testing框架。

但是你怎么能用程序语言进行unit testing呢? 还是需要在C中编写OO?

unit testing只需要“切面”或可以进行测试的边界。 测试不调用其他函数的C函数或仅调用其他函数的C函数非常简单。 这样的一些示例是执行计算或逻辑运算的function,并且本质上是function性的。 function在同一输入总是产生相同输出的意义上。 测试这些function可以带来巨大的好处,即使它只是通常被认为是unit testing的一小部分。

更复杂的测试,例如使用模拟或存根也是可能的,但它不像在更动态的语言中那样容易,或者甚至只是像C ++这样的面向对象的语言。 解决这个问题的一种方法是使用#defines。 这方面的一个例子就是本文unit testingOpenGL应用程序 ,它展示了如何模拟OpenGL调用。 这允许您测试OpenGL调用的有效序列。

另一种选择是利用弱符号。 例如,所有MPI API函数都是弱符号,因此如果在自己的应用程序中定义相同的符号,则实现将覆盖库中的弱实现。 如果库中的符号不​​弱,则在链接时会出现重复的符号错误。 然后,您可以实现有效的整个MPI C API的模拟,它允许您确保正确匹配调用,并且没有任何可能导致死锁的额外调用。 也可以使用dlopen()dlsym()加载库的弱符号,并在必要时传递调用。 MPI实际上提供了强大的PMPI符号,因此没有必要使用dlopen()和朋友。

您可以实现Cunit testing的许多好处。它稍微难以实现,并且可能无法获得您用Ruby或Java编写的内容所能达到的相同级别的覆盖率,但它绝对值得做。

在最基本的层面上,unit testing只是执行其他代码位的代码,并告诉您它们是否按预期工作。

您可以使用main()函数创建一个新的控制台应用程序,该应用程序执行一系列测试function。 每个测试都会在您的应用程序中调用一个函数,并返回0表示成功或另一个值表示失败。

我会给你一些示例代码,但我真的很生气。我确信有一些框架可以让它更容易一些。

您可以使用libtap ,它提供了许多function,可以在测试失败时提供诊断function。 它的一个例子:

 #include  #include  int main () { plan(3); ok(foo(), "foo returns 1"); is(bar(), "bar", "bar returns the string bar"); cmp_ok(baz(), ">", foo(), "baz returns a higher number than foo"); done_testing; } 

它类似于其他语言的tap库。

对于单独测试小块代码,没有任何本质上面向对象的东西。 在过程语言中,您可以测试函数和集合。

如果你是绝望的,而且你必须绝望,我将一个小的C预处理器和基于gmake的框架组合在一起。 它起初是一个玩具,从未真正长大,但我用它来开发和测试几个中等规模(10,000+线)的项目。

Dave的unit testing是最小的侵入,但它可以做一些我原先认为对于基于预处理器的框架不可能的测试(你可以要求某段代码在某些条件下抛出分段错误,它会为你测试它)。

这也是为什么大量使用预处理器很难安全地进行的一个例子。

进行unit testing的最简单方法是构建一个简单的驱动程序代码,该代码与其他代码链接,并在每种情况下调用每个函数……并断言函数结果的值并逐位构建。 ..无论如何我都是这样做的

 int main(int argc,char ** argv){

    //调用一些函数
    int x = foo();

   断言(x> 1);

    // 等等....

 }

希望这会有所帮助,最好的问候,汤姆。

下面是一个示例,说明如何在单个测试程序中为可能调用库函数的给定函数实现多个测试。

假设我们要测试以下模块:

 #include  int my_div(int x, int y) { if (y==0) exit(2); return x/y; } 

然后我们创建以下测试程序:

 #include  #include  #include  #include  // redefine assert to set a boolean flag #ifdef assert #undef assert #endif #define assert(x) (rslt = rslt && (x)) // the function to test int my_div(int x, int y); // main result return code used by redefined assert static int rslt; // variables controling stub functions static int expected_code; static int should_exit; static jmp_buf jump_env; // test suite main variables static int done; static int num_tests; static int tests_passed; // utility function void TestStart(char *name) { num_tests++; rslt = 1; printf("-- Testing %s ... ",name); } // utility function void TestEnd() { if (rslt) tests_passed++; printf("%s\n", rslt ? "success" : "fail"); } // stub function void exit(int code) { if (!done) { assert(should_exit==1); assert(expected_code==code); longjmp(jump_env, 1); } else { _exit(code); } } // test case void test_normal() { int jmp_rval; int r; TestStart("test_normal"); should_exit = 0; if (!(jmp_rval=setjmp(jump_env))) { r = my_div(12,3); } assert(jmp_rval==0); assert(r==4); TestEnd(); } // test case void test_div0() { int jmp_rval; int r; TestStart("test_div0"); should_exit = 1; expected_code = 2; if (!(jmp_rval=setjmp(jump_env))) { r = my_div(2,0); } assert(jmp_rval==1); TestEnd(); } int main() { num_tests = 0; tests_passed = 0; done = 0; test_normal(); test_div0(); printf("Total tests passed: %d\n", tests_passed); done = 1; return !(tests_passed == num_tests); } 

通过重新定义assert来更新布尔变量,如果断言失败并运行多个测试,则可以继续执行,跟踪成功的次数和失败次数。

在每个测试开始时,将rsltassert宏使用的变量)设置为1,并设置控制存根函数的任何变量。 如果您的一个存根被多次调用,您可以设置控制变量数组,以便存根可以检查不同调用的不同条件。

由于许多库函数都是弱符号,因此可以在测试程序中重新定义它们,以便它们被调用。 在调用要测试的函数之前,可以设置一些状态变量来控制存根函数的行为并检查函数参数的条件。

如果您无法像这样重新定义,请为存根函数指定一个不同的名称,并在代码中重新定义要测试的符号。 例如,如果您想要存根fopen但发现它不是弱符号,请将存根定义为my_fopen并使用-Dfopen=my_fopen编译该文件以进行测试。

在这种特殊情况下,要测试的function可以调用exit 。 这很棘手,因为exit无法返回到正在测试的function。 这是使用setjmplongjmp时很少见的事情之一。 在输入要测试的函数之前使用setjmp ,然后在存根的exit调用longjmp直接返回到测试用例。

另请注意,重新定义的exit有一个特殊变量,它会检查您是否确实要退出程序并调用_exit来执行此操作。 如果您不这样做,您的测试程序可能不会干净地退出。

此测试套件还计算尝试和失败测试的数量,如果所有测试都通过则返回0,否则返回1。 这样, make可以检查测试失败并采取相应措施。

以上测试代码将输出以下内容:

 -- Testing test_normal ... success -- Testing test_div0 ... success Total tests passed: 2 

返回码为0。

我使用断言。 但它并不是一个真正的框架。

使用C,它必须比简单地在现有代码之上实现框架更进一步。

我一直做的一件事是制作一个测试模块(带有一个主要版本),你可以运行很少的测试来测试你的代码。 这允许您在代码和测试周期之间进行非常小的增量。

更大的问题是编写代码以使其可测试。 专注于不依赖于共享变量或状态的小型独立函数。 尝试以“function”方式编写(没有状态),这将更容易测试。 如果您的依赖项不能始终存在或者速度很慢(如数据库),则可能必须编写一个完整的“模拟”层,可以在测试期间替换您的数据库。

主要unit testing目标仍然适用:确保被测代码始终重置为给定状态,不断测试等等……

当我在C中编写代码时(在Windows之前),我有一个批处理文件可以调出编辑器,然后当我完成编辑并退出时,它将编译,链接,执行测试,然后使用构建结果调出编辑器,测试结果和不同窗口中的代码。 在我rest之后(一分钟到几个小时取决于正在编译的内容)我可以只查看结果并直接回到编辑。 我相信这些日子可以改进这个过程:)