编写单元测试C代码

Writing unit tests for C code

我是一名C ++开发人员,在测试时,通过注入依赖项,覆盖成员函数等来测试类很容易,这样您就可以轻松地测试边缘案例。 但是,在C中,您无法使用这些精彩的功能。 我发现很难将单元测试添加到代码中,因为编写C代码的一些"标准"方法。 解决以下问题的最佳方法是:

传递一个大的'context'结构指针:

1
2
3
4
void some_func( global_context_t *ctx, .... )
{
  /* lots of code, depending on the state of context */
}

没有简单的方法来测试依赖函数的失败:

1
2
3
4
5
6
7
8
void some_func( .... )
{
  if (!get_network_state() && !some_other_func()) {
    do_something_func();
    ....
  }
  ...
}

具有大量参数的函数:

1
2
3
4
void some_func( global_context_t *, int i, int j, other_struct_t *t, out_param_t **out, ...)
{
  /* hundreds and hundreds of lines of code */
}

静态或隐藏功能:

1
2
3
4
5
6
7
8
9
10
static void foo( ... )
{
  /* some code */
}

void some_public_func( ... }
{
  /* call static functions */
  foo( ... );
}


一般来说,我同意Wes的回答 - 将测试添加到不是用测试编写的代码中会更加困难。 C中没有任何内在的东西使其无法测试 - 但是,由于C不会强迫您以特定的方式编写,因此编写难以测试的C代码也非常容易。

在我看来,编写带有测试的代码会鼓励更短的函数,只需要很少的参数,这有助于减轻示例中的一些痛苦。

首先,您需要选择一个单元测试框架。这个问题中有很多例子(尽管很多答案都是C ++框架 - 我建议不要使用C ++来测试C)。

我个人使用TestDept,因为它使用简单,重量轻,并且允许存根。但是,我认为它还没有被广泛使用。如果您正在寻找更受欢迎的框架,很多人会推荐Check - 如果您使用automake,那就太棒了。

以下是您的用例的一些具体答案:

Passing around a large 'context' struct pointer

对于这种情况,您可以在手动设置pre条件的情况下构建struct的实例,然后在函数运行后检查struct的状态。使用短函数,每个测试都相当简单。

No easy way to test failure on dependent functions

我认为这是单元测试C的最大障碍之一。
我已成功使用TestDept,它允许依赖函数的运行时存根。这对于分解紧密耦合的代码非常有用。以下是他们的文档中的示例:

1
2
3
4
5
void test_stringify_cannot_malloc_returns_sane_result() {
  replace_function(&malloc, &always_failing_malloc);
  char *h = stringify('h');
  assert_string_equals("cannot_stringify", h);
}

根据您的目标环境,这可能适用于您,也可能不适合您。有关详细信息,请参阅其文档。

Functions with lots of parameters

这可能不是您正在寻找的答案,但我只是将它们分解为具有较少参数的较小函数。更容易测试。

Static or hidden functions

它不是超级干净,但我通过直接包含源文件测试静态函数,启用静态函数调用。结合TestDept来删除任何未经测试的内容,这种方法效果相当不错。

1
2
3
 #include"implementation.c"

 /* Now I can call foo(), defined static in implementation.c */

许多C代码是遗留代码,几乎没有测试 - 在这些情况下,通常更容易添加首先测试大部分代码的集成测试,而不是细粒度的单元测试。这允许您开始将集成测试下的代码重构为可单元测试的状态 - 尽管根据您的具体情况,它可能或可能不值得投资。当然,您希望能够将单元测试添加到在此期间编写的任何新代码中,因此建立一个可靠的框架并提前运行是一个好主意。

如果您正在使用遗留代码,请阅读本书
(与Michael Feathers的遗留代码一起有效地工作)是一个很好的进一步阅读。


这是一个非常好的问题,旨在吸引人们相信C ++比C更好,因为它更容易测试。然而,这并不是那么简单。

编写了大量可测试的C ++和C代码,以及同样令人印象深刻的不可测试的C ++和C代码,我可以保密地说,你可以用两种语言包装糟糕的不可测试代码。实际上,您在上面提出的大多数问题在C ++中同样存在问题。 EG,很多人用C ++编写非对象封装函数并在类中使用它们(参见类中C ++静态函数的广泛使用,例如MyAscii :: fromUtf8()类型函数)。

而且我很确定您已经看到了具有太多参数的大量C ++类函数。如果你认为仅仅因为一个函数只有一个参数就更好了,那么考虑一下它在内部经常使用一堆成员变量屏蔽传入参数的情况。更别说"静态或隐藏"功能(提示,请记住"private:"关键字)同样大的问题。

所以,对你的问题的真正答案不是"因为你说的理由,C会更糟",而是"你需要在C中正确地构建它,就像在C ++中一样"。例如,如果您有依赖函数,则将它们放在不同的文件中,并在测试超级函数时通过实现该函数的伪版本来返回它们可能提供的可能答案的数量。而这几乎没有变化。如果要测试它们,请不要创建静态或隐藏功能。

真正的问题是,你似乎在你的问题中说明你正在为别人的库编写测试,而你没有编写测试,而架构师则需要适当的可测试性。然而,有大量的C ++库表现出完全相同的症状,如果你被交给其中一个进行测试,你就会同样恼火。

像这样的所有问题的解决方案总是一样的:正确编写代码,不要使用别人不正确编写的代码。


在单元测试C时,通常在测试中包含.c文件,这样您就可以在测试公共函数之前先测试静态函数。

如果您有复杂的函数并且想要测试调用它们的代码,则可以使用模拟对象。看看cmocka单元测试框架,它提供了对模拟对象的支持。