18

我是一名 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( ... );
}
4

3 回答 3

14

总的来说,我同意 Wes 的回答——将测试添加到没有考虑测试的代码中会变得更加困难。C 中没有什么固有的东西使测试变得不可能 - 但是,因为 C 不会强迫您以特定的风格编写,所以编写难以测试的 C 代码也很容易。

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

首先,您需要选择一个单元测试框架。这个问题有很多例子(虽然很遗憾很多答案都是 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);
}

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

有很多参数的函数

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

静态或隐藏函数

它不是超级干净,但我通过直接包含源文件来测试静态函数,启用静态函数的调用。结合 TestDept 来剔除任何不在测试中的东西,这工作得相当好。

 #include "implementation.c"

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

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

如果您正在使用遗留代码,这本书 (Michael Feathers 的《有效地使用遗留代码》)是很好的进一步阅读。

于 2013-06-07T02:03:31.413 回答
9

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

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

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

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

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

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

于 2013-06-07T01:16:54.837 回答
1

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

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

于 2013-12-20T13:42:39.480 回答