单元测试是保证代码质量的重要手段。本文将介绍 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,以及轻量级的自研框架。选择合适的测试框架,建立完善的测试习惯,让你的代码更加可靠。