代码之家  ›  专栏  ›  技术社区  ›  helpermethod

如何用普通的C语言编写单元测试?

  •  19
  • helpermethod  · 技术社区  · 16 年前

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

    但是如何用程序语言进行单元测试呢?或者需要在C中编程oo?

    8 回复  |  直到 16 年前
        1
  •  19
  •   bstpierre Edgar Aviles    15 年前

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

    更复杂的测试,如使用模拟或存根也是可能的,但它不像在更动态的语言中那样容易,甚至只是面向对象的语言,如C++。一种方法是使用定义。本文就是一个例子, Unit testing OpenGL applications 它演示了如何模拟OpenGL调用。这允许您测试是否进行了有效的OpenGL调用序列。

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

    您可以认识到C的单元测试的许多好处。它稍微困难一点,并且可能不可能获得您在Ruby或Java中编写的相同级别的覆盖率,但它确实值得做。

        2
  •  12
  •   Pascal Cuoq    16 年前

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

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

    我会给你一些示例代码,但是我对C很生疏。我确信有一些框架可以让这变得更简单。

        3
  •  6
  •   Jake    15 年前

    你可以用 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库。

        4
  •  4
  •   dmckee    16 年前

    在隔离测试代码的小部分方面,没有什么本质上面向对象的。在程序语言中,测试函数及其集合。

    如果你绝望了,你也不得不绝望,我拼凑了一个小的C预处理器和基于gmake的框架。它开始是个玩具,从未真正长大,但我 有 用于开发和测试几个中型(10000多条线路)项目。

    Dave's Unit Test 它具有最小的侵入性,但它可以做一些我最初认为基于预处理器的框架不可能做的测试(您可以要求在某些条件下,某段代码抛出一个分段错误,它将为您测试)。

    这也是为什么大量使用预处理器的一个例子 坚硬的 安全地做。

        5
  •  3
  •   t0mm13b    16 年前

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

    int main(int argc, char **argv){
    
       // call some function
       int x = foo();
    
       assert(x > 1);
    
       // and so on....
    
    }
    

    希望这有帮助, 顺祝商祺! 汤姆.

        6
  •  3
  •   dbush    10 年前

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

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

    #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 要更新布尔变量,可以在断言失败时继续,并运行多个测试,跟踪成功的数量和失败的数量。

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

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

    如果不能这样重新定义,请给存根函数一个不同的名称,并重新定义要测试的代码中的符号。例如,如果您想要存根 fopen 但发现它不是一个弱符号,将存根定义为 my_fopen 编译要测试的文件 -Dfopen=my_fopen .

    在这种特殊情况下,要测试的函数可能调用 exit . 这很棘手,因为 出口 无法返回正在测试的函数。这是少数有意义的时候之一 setjmp 和 longjmp . 你使用 塞特姆 在进入要测试的功能之前,然后在存根中 出口 你打电话来 朗吉姆普 直接返回测试用例。

    还要注意,重新定义的 出口 有一个特殊的变量,它检查您是否真的想退出程序并调用 _exit 这样做。如果不这样做,您的测试程序可能不会完全退出。

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

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

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

    返回码为0。

        7
  •  1
  •   Luca Matteis    16 年前

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

        8
  •  1
  •   Peter Mortensen Pieter Jan Bonestroo    16 年前

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

    我一直在做的一件事是创建一个测试模块(带有一个主模块),您可以从中运行一些测试来测试代码。这允许您在代码和测试周期之间做非常小的增量。

    更大的问题是编写可测试的代码。专注于不依赖共享变量或状态的小型独立函数。尝试以“功能性”的方式写作(无状态),这将更容易测试。如果您有一个不能总是存在或速度慢的依赖项(如数据库),那么您可能需要编写一个完整的“模拟”层,在测试期间可以用它代替您的数据库。

    单元测试的主要目标仍然适用:确保被测试的代码总是重置到一个给定的状态,不断地测试,等等。

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