为 C 代码编写单元测试

Posted

技术标签:

【中文标题】为 C 代码编写单元测试【英文标题】:Writing unit tests for C code 【发布时间】:2013-06-02 22:19:46 【问题描述】:

我是一名 C++ 开发人员,在测试方面,通过注入依赖项、覆盖成员函数等来测试类很容易,这样您就可以轻松测试边缘情况。但是,在 C 中,您不能使用那些美妙的功能。由于编写 C 代码的某些“标准”方式,我发现很难将单元测试添加到代码中。解决以下问题的最佳方法是什么:

传递一个大的“上下文”结构指针:

void some_func( global_context_t *ctx, .... )

  /* lots of code, depending on the state of context */

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

void some_func( .... )

  if (!get_network_state() && !some_other_func()) 
    do_something_func();
    ....
  
  ...

具有大量参数的函数:

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 */

静态或隐藏函数:

static void foo( ... )

  /* some code */
 

void some_public_func( ... 

  /* call static functions */
  foo( ... );

【问题讨论】:

Possible duplicate,如果不只是相关的话。 @ajp15243:我对单元测试时使用什么框架或工具不太感兴趣。相反,我对为高度依赖或使用常见 C 习惯用法的 C 代码编写测试的方法感兴趣,例如传递的 global_context_t 结构。 好吧,如果可以的话,我会编辑掉“重复”部分。以为您至少会发现这些框架很有用,但是哦,好吧:/. 简短的回答是在编写 C 代码和单元测试时必须考虑 C。不要尝试将 C 用作 C++(也不要反过来,就此而言),您就是做不到。 嗯。我会说简短的回答是那是相当糟糕的c代码。 c 程序员应该像其他人一样为测试编写代码。 【参考方案1】:

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

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

【讨论】:

【参考方案2】:

总的来说,我同意 Wes 的回答 - 将测试添加到没有考虑测试的代码中会更加困难。 C 语言本身并没有什么东西无法测试 - 但是,由于 C 语言不会强迫您以特定的风格编写,因此编写难以测试的 C 代码也很容易。

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

首先,您需要选择一个单元测试框架。 this question 中有很多示例(尽管遗憾的是很多答案都是 C++ 框架——我建议不要使用 C++ 来测试 C)。

我个人使用TestDept,因为它使用简单、轻量级并且允许存根。但是,我认为它还没有被广泛使用。如果你正在寻找更流行的框架,很多人推荐Check - 如果你使用 automake,那就太好了。

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

传递一个大的“上下文”结构指针

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

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

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

void test_stringify_cannot_malloc_returns_sane_result() 
  replace_function(&malloc, &always_failing_malloc);
  char *h = stringify('h');
  assert_string_equals("cannot_stringify", h);

根据您的目标环境,这可能适合您,也可能不适合您。详情请见their documentation。

具有大量参数的函数

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

静态或隐藏函数

它不是超级干净,但我通过直接包含源文件来测试静态函数,启用静态函数的调用。与 TestDept 结合使用可以剔除任何未测试的内容,这非常有效。

 #include "implementation.c"

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

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

如果您正在使用遗留代码,this book (Michael Feathers 的《有效处理遗留代码》)是很好的进一步阅读材料。

【讨论】:

答案也不错;来自我的 +1 :-)【参考方案3】:

这是一个很好的问题,旨在诱使人们相信 C++ 比 C 更好,因为它更易于测试。然而,事情并没有那么简单。

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

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

因此,您问题的真正答案不是“由于您所说的原因,C 更糟糕”,而是“您需要在 C 中正确地构建它,就像在 C++ 中一样”。例如,如果您有依赖函数,则将它们放在不同的文件中,并在测试超级函数时通过实现该函数的虚假版本返回它们可能提供的答案数量。这就是勉强得到的改变。如果您想测试它们,请不要制作静态或隐藏函数。

真正的问题是,您似乎在问题中声明您正在为其他人的库编写测试,而这些测试不是您编写的,并且是为适当的可测试性而设计的。但是,有大量的 C++ 库表现出完全相同的症状,如果您被交给​​其中一个进行测试,您同样会感到恼火。

所有此类问题的解决方案总是相同的:正确编写代码,不要使用其他人编写不当的代码。

【讨论】:

本垒打。 +1 告诉它是这样的。 谢谢@RandyHoward,非常感谢您的称赞。 很好的建议,我也赞成。我认为 C 在事后添加测试方面比其他一些语言更宽容,这导致了坏名声。但是,您可以绝对编写干净、可测试的 C。 谢谢@TimothyJones。我同意,你绝对可以第一次写好 C。而且我承认 C++ 的某些元素使编写测试变得更容易。当然,任何一个事物大于或等于另一个事物的功能都意味着一个事物将在某些领域取得胜利。所以是的,如果类构造良好,为 C++ 编写一些测试会更容易。但是两种语言都必须构造良好,以便任何一种都可测试。 当然。就个人而言,我宁愿面对无法测试的 C 而不是无法测试的 C++ :)

以上是关于为 C 代码编写单元测试的主要内容,如果未能解决你的问题,请参考以下文章

C# - 单元测试,模拟?

为 Moya 请求编写单元测试

单元测试和实现之间的重复代码

为 UIView 框架编写单元测试

如何为以下代码段编写单元测试用例

Vala 的单元测试框架