如何在普通C中编写单元测试?

时间:2010-02-26 17:13:33

标签: c unit-testing

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

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

8 个答案:

答案 0 :(得分:20)

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

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

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

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

答案 1 :(得分:13)

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

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

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

答案 2 :(得分:6)

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

#include <mystuff.h>
#include <tap.h>

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库。

答案 3 :(得分:4)

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

如果你是绝望的,而且你必须绝望,我就会把一个小的C预处理器和基于gmake的框架拼凑在一起。它起初是一个玩具,从未真正成长过,但我已经用它来开发和测试几个中等规模(10,000多行)的项目。

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

这也是为什么大量使用预处理器 hard 安全地做的原因。

答案 4 :(得分:3)

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

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

   // call some function
   int x = foo();

   assert(x > 1);

   // and so on....

}

希望这有帮助, 最好的祝福, 汤姆。

答案 5 :(得分:3)

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

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

#include <stdlib.h>

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

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

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <setjmp.h>

// 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进行测试。

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

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

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

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

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

返回代码为0。

答案 6 :(得分:1)

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

答案 7 :(得分:1)

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

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

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

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

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