单元测试 C 代码

今年夏天,我使用 C 语言编写了一个嵌入式系统。这是我工作的公司接手的一个现有项目。我已经非常习惯于使用 JUnit 在 Java 中编写单元测试,但是对于为现有代码(需要重构)以及添加到系统中的新代码编写单元测试的最佳方式感到困惑。

是否有任何项目可以像使用 JUnit 对 Java 代码进行单元测试那样简单地对普通 C 代码进行单元测试?非常感谢任何专门用于嵌入式开发(对 arm-linux 平台进行交叉编译)的见解。

答案

C 语言中的一个单元测试框架是Check ; 可以在这里找到 C 语言中的单元测试框架列表,并在下面复制。根据您的运行时具有多少标准库功能,您是否可以使用其中之一。

AceUnit

AceUnit(高级 C 和嵌入式单元)将自己视为一个舒适的 C 代码单元测试框架。它试图模仿 JUnit 4.x,并包括类似反射的功能。 AceUnit 可以用于资源受限的环境中,例如嵌入式软件开发,并且重要的是,它可以在无法包含单个标准头文件并且不能从 ANSI / ISO C 库调用单个标准 C 函数的环境中正常运行。它还具有 Windows 端口。尽管作者已经表示有兴趣添加这种功能,但它不使用分叉来捕获信号。请参阅AceUnit 主页

GNU Autounit

与 Check 大致相同,包括分叉在单独的地址空间中运行单元测试(实际上,Check 的原始作者是从 GNU Autounit 借用此想法的)。 GNU Autounit 广泛使用 GLib,这意味着链接等需要特殊的选项,但这对您来说不是一个大问题,特别是如果您已经在使用 GTK 或 GLib。参见GNU Autounit 主页

单位

也使用 GLib,但不分叉以保护单元测试的地址空间。

单位

标准 C,带有 Win32 GUI 实现的计划。当前不分叉或以其他方式保护单元测试的地址空间。在早期开发中。参见CUnit 主页

铜测试

一个只有一个. c 和一个. h 文件的简单框架,您将其放入源代码树中。请参阅CuTest 主页

CppUnit

一流的 C ++ 单元测试框架;您还可以使用它来测试 C 代码。它稳定,积极开发,并具有 GUI 界面。不使用 CppUnit for C 的主要原因是首先,它很大,其次,您必须使用 C ++ 编写测试,这意味着您需要 C ++ 编译器。如果这些听起来不令人担忧,那么与其他 C ++ 单元测试框架一起绝对值得考虑。请参阅CppUnit 主页

embUnit

embUnit(嵌入式单元)是嵌入式系统的另一个单元测试框架。这似乎已被 AceUnit 取代。 嵌入式单元主页

最小单位

最小的宏集就可以了!关键是要说明对单元代码进行单元测试的难易程度。参见MinUnit 主页

安藤先生的 CUnit

这是一个相当新的 CUnit 实现,并且显然仍处于早期开发中。有关Ando 先生的主页,请参见CUnit

此列表的最新更新时间为 2008 年 3 月。

更多框架:

卡莫卡

CMocka 是 C 的测试框架,支持模拟对象。易于使用和设置。

请参阅CMocka 主页

标准

Criterion 是一个跨平台的 C 单元测试框架,支持自动测试注册,参数化测试,理论,并且可以输出为多种格式,包括 TAP 和 JUnit XML。每个测试都在其自己的过程中运行,因此可以根据需要报告或测试信号和崩溃。

有关更多信息,请参见Criterion 主页

棚屋

HWUT 是具有 C 的强大支持的通用单元测试工具。它可以帮助创建 Makefile,生成用最小的 “迭代表” 编码的大量测试用例,遍历状态机,生成 C 存根等等。一般方法非常独特:裁决基于 “好的标准输出 / 不好的标准输出”。但是,比较功能很灵活。因此,任何类型的脚本都可以用于检查。它可以应用于可以产生标准输出的任何语言。

请参阅HWUT 主页

绿色

适用于 C 和 C ++ 的现代,可移植,跨语言单元测试和模拟框架。它提供了可选的 BDD 表示法,模拟库以及在单个进程中运行它的能力(使调试更加容易)。可以自动发现测试功能的测试运行程序。但是您可以以编程方式创建自己的。

所有这些功能(以及更多功能)在CGreen 手册中进行了说明。

Wikipedia 在 “单元测试框架列表:C”下给出了 C 单元测试框架的详细列表。

我个人喜欢Google Test 框架

测试 C 代码的真正困难在于打破对外部模块的依赖,因此您可以将代码隔离为单元。当您尝试对遗留代码进行测试时,这尤其成问题。在这种情况下,我经常发现自己使用链接器在测试中使用存根函数。

这就是人们谈论 “ 接缝 ” 时所指的意思。在 C 语言中,您唯一的选择确实是使用预处理器或链接器来模拟您的依赖项。

我的一个 C 项目中的典型测试套件可能如下所示:

#include "myimplementationfile.c"
#include <gtest/gtest.h>

// Mock out external dependency on mylogger.o
void Logger_log(...){}

TEST(FactorialTest, Zero) {
    EXPECT_EQ(1, Factorial(0));
}

请注意,您实际上包括的是 C 文件,而不是头文件 。这具有访问所有静态数据成员的优势。在这里,我模拟了我的记录器(可能在 logger.o 中,并提供了一个空的实现。这意味着测试文件独立于其余代码库进行编译和链接,并独立执行。

至于交叉编译代码,要使其正常工作,您需要在目标设备上具有良好的功能。我通过在 PowerPC 架构上编译为 Linux 的 googletest cross 来完成此任务。这是有道理的,因为那里有一个完整的 shell 和 os 来收集结果。对于不太丰富的环境(我将其归类为没有完整操作系统的任何东西),您应该仅在主机上构建并运行。无论如何,您都应该这样做,以便您可以在构建过程中自动运行测试。

我发现测试 C ++ 代码通常要容易得多,这是因为 OO 代码通常比过程耦合少(当然,这在很大程度上取决于编码样式)。同样在 C ++ 中,您可以使用诸如依赖项注入和方法重写之类的技巧将接缝插入以其他方式封装的代码中。

迈克尔 · 费瑟斯(Michael Feathers)有一本关于测试遗留代码的好书 。在第一章中,他介绍了我强烈推荐的处理非 OO 代码的技术。

编辑 :我写了一篇有关单元测试过程代码的博客文章 ,其源代码可以在 GitHub 上找到

编辑从实用程序员那里有一本新书 ,特别针对我强烈推荐的单元测试 C 代码。

Minunit是一个非常简单的单元测试框架。我正在用它来对 AVR 的 C 微控制器代码进行单元测试。

我目前正在使用 CuTest 单元测试框架:

http://cutest.sourceforge.net/

它非常轻巧和简单,因此非常适合嵌入式系统。使它在目标平台和台式机上都能正常工作没有问题。除了编写单元测试之外,还需要:

  • 在调用 CuTest 例程的任何地方都包含头文件
  • 单个其他 “C” 文件将被编译 / 链接到映像中
  • 在 main 中添加了一些简单的代码来设置和调用单元测试 - 我只是将其放在特殊的 main()函数中,如果在构建过程中定义了 UNITTEST,则该函数会进行编译。

系统需要支持堆和某些 stdio 功能(并非所有嵌入式系统都具有)。但是代码很简单,如果您的平台没有这些需求,您可能可以替代这些需求。

明智地使用 extern“C” {} 块,它也支持测试 C ++。

我说的与 ratkok 几乎相同,但是如果您对单元测试有内在的扭曲,那么...

Unity-强烈建议对 C 代码进行单元测试的框架。

本书中针对嵌入式 C 的线程TDD 中提到的示例是使用 Unity(和 CppUTest)编写的。

您可能还需要看一下libtap ,这是一个 C 测试框架,可以输出 “测试任何协议”(TAP),从而与针对该技术的各种工具很好地集成在一起。它主要用于动态语言领域,但易于使用且非常受欢迎。

一个例子:

#include <tap.h>

int main () {
    plan(5);

    ok(3 == 3);
    is("fnord", "eek", "two different strings not that way?");
    ok(3 <= 8732, "%d <= %d", 3, 8732);
    like("fnord", "f(yes|no)r*[a-f]$");
    cmp_ok(3, ">=", 10);

    done_testing();
}

有一个优雅的 C 单元测试框架,它支持称为cmocka 的模拟对象。它只需要标准 C 库,就可以在各种计算平台(包括嵌入式)和不同的编译器上运行。

它还支持不同的消息输出格式,例如 Subunit,Test Anything Protocol 和 jUnit XML 报告。

cmocka 被创建为还可以在嵌入式平台上运行,并且还具有 Windows 支持。

一个简单的测试如下所示:

#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>

/* A test case that does nothing and succeeds. */
static void null_test_success(void **state) {
    (void) state; /* unused */
}

int main(void) {
    const struct CMUnitTest tests[] = {
        cmocka_unit_test(null_test_success),
    };
    return cmocka_run_group_tests(tests, NULL, NULL);
}

API已有完整文档,并且一些示例是源代码的一部分。

要开始使用 cmocka,您应该阅读 LWN.net 上的文章: 使用 C 中的模拟对象进行单元测试

cmocka 1.0 已于 2015 年 2 月发布。

在开始寻找一种模拟函数的方法之前,我并没有对遗留 C 应用程序进行过广泛的测试。我非常需要模拟才能将我要测试的 C 文件与其他文件隔离开。我尝试了 cmock,我想我会采用它。

Cmock 扫描头文件并根据找到的原型生成模拟函数。 Mocks 可以让您完美隔离地测试 C 文件。您要做的就是将测试文件与模拟链接而不是真实的目标文件链接。

cmock 的另一个优点是它将验证传递给模拟函数的参数,并使您可以指定模拟应提供的返回值。这对于测试函数中不同的执行流程非常有用。

测试由典型的 testA()和 testB()函数组成,您可以在其中构建期望,调用函数以测试和检查断言。

最后一步是为您的测试生成统一的运行器。 Cmock 与统一测试框架相关。 Unity 与其他任何单元测试框架一样易于学习。

值得一试并且很容易理解:

http://sourceforge.net/apps/trac/cmock/wiki

更新 1

我正在研究的另一个框架是 Cmockery。

http://code.google.com/p/cmockery/

它是一个纯 C 框架,支持单元测试和模拟。它与 ruby 无关(与 Cmock 相反),对外部库的依赖性很小。

由于它不会生成代码,因此需要更多的手动工作来设置模拟。因为原型不会发生太大变化,所以对于现有项目而言,这并不意味着很多工作:一旦有了模拟,就无需在一段时间内进行更改(这就是我的情况)。额外的输入可以完全控制模拟。如果您不喜欢某些东西,只需更改您的模拟即可。

无需特殊的测试跑步者。您只需要创建一个测试数组并将其传递给 run_tests 函数。这里也需要更多的手动工作,但是我绝对喜欢一个独立的自治框架的想法。

另外,它包含一些我不知道的漂亮 C 技巧。

总体而言,Cmockery 需要对模拟有更多的了解才能上手。示例可以帮助您克服这一问题。看起来它可以用更简单的机制完成任务。

作为 C 的新手,我发现 C 中名为 “ 测试驱动的开发”的幻灯片非常有用。基本上,它使用标准的assert()&&来传递消息,而没有任何外部依赖关系。如果有人习惯了完整的堆栈测试框架,则可能不会:)

我们编写了CHEAT (托管在GitHub 上 )以提高易用性和可移植性。

它没有依赖关系,不需要安装或配置。只需要一个头文件和一个测试用例。

#include <cheat.h>

CHEAT_TEST(mathematics_still_work,
    cheat_assert(2 + 2 == 4);
    cheat_assert_not(2 + 2 == 5);
)

测试将编译为可执行文件,该可执行文件负责运行测试并报告其结果。

$ gcc -I . tests.c
$ ./a.out
..
---
2 successful of 2 run
SUCCESS

它也有漂亮的颜色。