如何用C编写单元测试?

eblbsuwk  于 2023-02-15  发布在  其他
关注(0)|答案(9)|浏览(197)

我开始深入研究GLib文档,发现它也提供了一个单元测试框架。
但是你怎么能用过程语言做单元测试呢?或者它需要用C编写OO吗?

mzsu5hc0

mzsu5hc01#

单元测试只需要“切面”或边界,在此边界上可以进行测试。测试不调用其他函数或只调用其他被测试函数的C函数是非常直接的。这方面的一些例子是执行计算或逻辑运算的函数。并且在本质上是功能性的。功能性是指相同的输入总是导致相同的输出。测试这些功能可以有巨大的好处,尽管它只是通常被认为是单元测试的一小部分。
更复杂的测试,例如使用模拟或存根也是可能的,但是它远不如在更动态的语言中,甚至不如在面向对象的语言(例如C++)中那么容易。实现这一点的一种方法是使用#defines。这篇文章Unit testing OpenGL applications就是一个例子。它展示了如何模拟OpenGL调用。这允许您测试是否进行了有效的OpenGL调用序列。
另一种选择是利用弱符号。例如,所有MPI API函数都是弱符号,因此如果您在自己的应用程序中定义相同的符号,则您的实现会覆盖库中的弱实现。如果库中的符号不是弱符号,则会在链接时得到重复的符号错误。然后,您可以实现整个MPI C API的有效模拟。这允许你确保调用正确匹配,并且没有任何额外的调用会导致死锁。也可以使用dlopen()dlsym()加载库的弱符号,并在必要时传递调用。MPI实际上提供了PMPI符号,这些符号很强,因此不必使用dlopen()和friends。
您可以认识到C单元测试的许多好处。它稍微困难一些,并且可能无法获得您期望从Ruby或Java编写的东西中获得的相同级别的覆盖率,但它绝对值得一试。

ngynwnxp

ngynwnxp2#

在最基本的级别上,单元测试只是执行其他代码位并告诉您它们是否按预期工作的代码位。
你可以简单地创建一个新的控制台应用程序,使用main()函数执行一系列测试函数,每个测试都会调用应用程序中的一个函数,如果成功,则返回0,如果失败,则返回另一个值。
我想给予你一些示例代码,但我对C语言真的很生疏。我相信有一些框架也会让这变得更容易。

f0brbegy

f0brbegy3#

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

7eumitmz

7eumitmz4#

下面是一个示例,说明如何在单个测试程序中为可能调用库函数的给定函数实现多个测试。
假设我们要测试以下模块:

#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来更新一个布尔变量,如果Assert失败,您可以继续并运行多个测试,跟踪成功和失败的次数。
在每个测试开始时,将rsltassert宏使用的变量)设置为1,并设置所有控制存根函数的变量。如果某个存根函数被多次调用,则可以设置控制变量数组,以便存根函数可以检查不同调用的不同条件。
由于许多库函数都是弱符号,因此可以在测试程序中重新定义它们,以便调用它们。在调用要测试的函数之前,可以设置一些状态变量来控制存根函数的行为,并检查函数参数的条件。
如果你不能像这样重新定义,给stub函数一个不同的名字,并重新定义代码中的符号来测试。例如,如果你想stub fopen,但发现它不是一个弱符号,定义你的stub为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。

vuv7lop3

vuv7lop35#

孤立地测试小段代码本质上并不是面向对象的,在过程语言中,你测试函数及其集合。
如果你绝望了,你必须绝望,我把一个小的C预处理器和基于gmake的框架组合在一起。它开始是一个玩具,从来没有真正成长起来,但我 * 已经 * 用它开发和测试了几个中等规模(10,000多行)的项目。
Dave's Unit Test的侵入性最小,但它可以做一些我最初认为基于预处理器的框架不可能做的测试(您可以要求某段代码在某些条件下抛出分段错误,它会为您测试)。
这也是一个例子,说明了为什么大量使用预处理器是“困难”的。

cfh9epnr

cfh9epnr6#

进行单元测试最简单的方法是构建一个简单的驱动程序代码,它与其他代码链接,并在每种情况下调用每个函数...Assert函数结果的值,然后一点一点地构建...反正我就是这么做的

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

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

   assert(x > 1);

   // and so on....

}

希望这个有用。

wkyowqbh

wkyowqbh7#

对于C语言,它必须比简单地在现有代码上实现一个框架更进一步。
我一直在做的一件事就是创建一个测试模块(带有main),你可以在其中运行一些小的测试来测试你的代码,这允许你在代码和测试周期之间做非常小的增量。
更大的问题是编写可测试的代码。关注那些不依赖于共享变量或状态的小的、独立的函数。尝试以“函数”的方式编写(没有状态),这将更容易测试。如果你有一个依赖关系,不能总是存在或很慢(像数据库),你可能不得不编写一个完整的“模拟”层,可以在测试过程中替换你的数据库。
主要单元测试目标仍然适用:确保受试代码始终重置为给定状态,持续测试等。
当我用C写代码时(回到Windows之前)我有一个批处理文件,它会调出一个编辑器,然后当我完成编辑并退出时,它会编译、链接、执行测试,然后调出带有构建结果的编辑器,测试结果和代码在不同的窗口.休息后(一分钟到几个小时取决于编译的内容)我可以只查看结果,然后直接回去编辑。我相信这个过程可以在这些天得到改进:)

zbdgwd5y

zbdgwd5y8#

我使用了assert,但它不是一个真正的框架。

r7knjye2

r7knjye29#

您可以自己编写一个简单的最小化测试框架:

// test_framework.h

#define BEGIN_TESTING int main(int argc, char **argv) {
#define END_TESTING return 0;}
#define TEST(TEST_NAME) if (run_test(TEST_NAME, argc, argv))

int run_test(const char* test_name, int argc, char **argv) {
    // we run every test by default
    if (argc == 1) { return 1; }
    // else we run only the test specified as a command line argument
    for (int i = 1; i < argc; i++) {
        if (!strcmp(test_name, argv[i])) { return 0; }
    } 
    return 0;
}

现在在实际测试文件中执行以下操作:

#include test_framework.h

BEGIN_TESTING

TEST("MyPassingTest") {
    assert(1 == 1);
}

TEST("MyFailingTest") {
    assert(1 == 2);
}

END_TESTING

如果要运行所有测试,请执行不带命令行参数的./binary,如果只想运行特定测试,请执行./binary MyFailingTest

相关问题