C语言单元测试完全指南

单元测试是保证代码质量的重要手段。本文将介绍 C 语言中常用的单元测试框架,包括 Google Test、Cmocka 以及轻量级的自研测试框架,帮助你建立完善的测试体系。

为什么需要单元测试

  • 尽早发现 bug:在开发阶段就发现问题,降低修复成本
  • 确保重构安全:修改代码后通过测试确保功能不受影响
  • 文档作用:测试用例本身就是最好的代码文档
  • 提高代码质量:编写可测试的代码通常意味着更好的设计

常用测试框架介绍

1. Google Test (gtest)

Google Test 是 Google 开发的 C++ 测试框架,但也可以用于 C 语言测试。它功能丰富,支持断言、测试夹具、参数化测试等高级功能。

/* mylib.c - 被测试的代码 */
#include "mylib.h"

int add(int a, int b) {
    return a + b;
}

int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}
/* mylib_test.cpp - Google Test 测试用例 */
#include 
extern "C" {
    #include "mylib.h"
}

/* 测试 add 函数 */
TEST(AddTest, PositiveNumbers) {
    EXPECT_EQ(add(2, 3), 5);
}

TEST(AddTest, NegativeNumbers) {
    EXPECT_EQ(add(-2, -3), -5);
}

TEST(AddTest, Zero) {
    EXPECT_EQ(add(0, 0), 0);
}

/* 测试 factorial 函数 */
TEST(FactorialTest, Basic) {
    EXPECT_EQ(factorial(5), 120);
    EXPECT_EQ(factorial(0), 1);
    EXPECT_EQ(factorial(1), 1);
}

int main(int argc, char **argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

2. Cmocka 框架

Cmocka 是一个纯 C 的单元测试框架,适合纯 C 项目,无需 C++ 编译环境。

/* test_calc.c - 使用 Cmocka */
#include 
#include stdlib.h
#include stdio.h
#include string.h
#include 

#include 

/* 要测试的函数 */
int divide(int a, int b, int *result) {
    if (b == 0) return -1;
    *result = a / b;
    return 0;
}

/* 测试用例 */
static void test_divide_success(void **state) {
    (void) state;
    int result;
    int ret = divide(10, 2, &result);
    assert_int_equal(ret, 0);
    assert_int_equal(result, 5);
}

static void test_divide_by_zero(void **state) {
    (void) state;
    int result;
    int ret = divide(10, 0, &result);
    assert_int_equal(ret, -1);
}

int main(void) {
    const UnitTest tests[] = {
        unit_test(test_divide_success),
        unit_test(test_divide_by_zero),
    };
    return run_tests(tests);
}

3. 轻量级自研测试框架

如果不想引入外部依赖,可以自己实现一个简单的测试框架:

/* minunit.h - 极简测试框架 */
#ifndef MINUNIT_H
#define MINUNIT_H

#include 
#include stdlib.h>

#define MU_ASSERT(msg, expr) \
    do { \
        if (!(expr)) { \
            printf("FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); \
            exit(1); \
        } \
    } while(0)

#define MU_ASSERT_EQ(msg, a, b) \
    do { \
        if ((a) != (b)) { \
            printf("FAIL: %s: %ld != %ld (%s:%d)\n", \
                   msg, (long)(a), (long(b), __FILE__, __LINE__); \
            exit(1); \
        } \
    } while(0)

#define MU_RUN_TEST(fn) \
    do { \
        printf("Running %s...\n", #fn); \
        fn(); \
    } while(0)

#define MU_SUCCESS \
    do { \
        printf("All tests passed!\n"); \
        return 0; \
    } while(0)

#endif
/* test_example.c - 使用自研框架 */
#include "minunit.h"

int add(int a, int b) { return a + b; }

void test_add_positive(void) {
    MU_ASSERT_EQ("2 + 3 = 5", add(2, 3), 5);
}

void test_add_negative(void) {
    MU_ASSERT_EQ("-1 + 1 = 0", add(-1, 1), 0);
}

void test_add_zero(void) {
    MU_ASSERT_EQ("0 + 0 = 0", add(0, 0), 0);
}

int main(void) {
    MU_RUN_TEST(test_add_positive);
    MU_RUN_TEST(test_add_negative);
    MU_RUN_TEST(test_add_zero);
    MU_SUCCESS;
}

测试夹具 (Fixtures)

测试夹具用于在测试前设置环境,测试后清理资源:

/* 使用 Google Test 夹具 */
class HashTableTest : public ::testing::Test {
protected:
    void SetUp() override {
        table = ht_create(100);
    }

    void TearDown() override {
        ht_destroy(table);
    }

    HashTable *table;
};

TEST_F(HashTableTest, InsertAndGet) {
    ht_insert(table, "key", "value");
    char *val = ht_get(table, "key");
    EXPECT_STREQ(val, "value");
}

TEST_F(HashTableTest, MissingKey) {
    char *val = ht_get(table, "nonexistent");
    EXPECT_NULL(val);
}

测试覆盖率

使用 gcov 和 lcov 可以生成代码覆盖率报告:

# 编译时添加覆盖率标志
gcc -fprofile-arcs -ftest-coverage -o test_program test.c

# 运行测试
./test_program

# 生成覆盖率报告
gcov test.c

# 生成 HTML 覆盖率报告
lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage_report

测试建议

  • 每个函数应该有对应的测试用例
  • 测试边界条件和异常情况
  • 保持测试的独立性,不依赖执行顺序
  • 使用有意义的测试名称
  • 定期运行测试,将其集成到 CI/CD 流程

总结

单元测试是保证 C 代码质量的有效手段。本文介绍了三种常用的测试方案:功能强大的 Google Test、纯 C 的 Cmocka,以及轻量级的自研框架。选择合适的测试框架,建立完善的测试习惯,让你的代码更加可靠。