3. 工具链集成核心概念

单元测试框架选型,说白了就是给项目挑一把最顺手的工具。我见过不少团队,框架选得不对,后面集成构建系统时各种踩坑。今天咱们就把 CUnit、Unity、CMock、Google Test 这四兄弟拉出来遛遛,看看它们各自适合什么场景。

3.1 四大框架核心差异

先看一张对比表,心里有个底:

特性 CUnit Unity CMock Google Test
语言 C C C(配合 Unity) C++
运行环境 桌面 Linux/Windows 嵌入式裸机/RTOS 嵌入式裸机/RTOS 桌面 Linux/Windows
Mock 支持 无 无 自动生成 Mock 有(需 GMock)
断言风格 CU_ASSERT 宏 TEST_ASSERT 宏 配合 Unity 断言 ASSERT_/EXPECT_ 宏
测试注册 手动注册 Suite 自动注册 自动注册 自动注册
资源占用 较大(依赖 libc) 极小(纯宏实现) 中等 较大(依赖 C++ 运行时)
典型场景 PC 端 C 项目 MCU 固件测试 嵌入式模块隔离测试 复杂 C++ 项目

我的选型原则:嵌入式裸机项目首选 Unity + CMock 组合;PC 端 C 项目用 CUnit 够用;C++ 项目直接上 Google Test。别想着一个框架通吃所有场景,那只会让你在集成时头疼。

3.2 框架选型决策流程

我习惯用下面这个决策树来快速判断:

单元测试框架选型决策树 项目语言? C 语言 C++ 语言 嵌入式裸机/RTOS 桌面 Linux/Windows Unity + CMock CUnit 复杂项目 简单项目 Google Test + GMock Google Test 选型要点: • 嵌入式裸机:资源受限,选 Unity(纯宏,无动态内存分配) • 需要 Mock:CMock 自动生成,省去手写桩代码的麻烦 • 桌面 C 项目:CUnit 轻量,集成简单 • C++ 项目:Google Test 功能最全,社区活跃

3.3 与构建系统集成实战

框架选好了,怎么把它塞进构建系统里?我分别用 Makefile 和 CMake 演示一下。

3.3.1 Unity + CMock 集成到 Makefile

这是我做 STM32 项目时最常用的组合。Unity 负责断言,CMock 负责生成 Mock 函数。

# Makefile 片段 - Unity + CMock 集成
UNITY_DIR = ./third_party/unity
CMOCK_DIR = ./third_party/cmock
MOCK_GENERATOR = ruby $(CMOCK_DIR)/lib/cmock.rb

# 生成 Mock 文件
MOCK_SRCS = $(patsubst %.h, %_Mock.c, $(MOCK_HEADERS))
MOCK_OBJS = $(MOCK_SRCS:.c=.o)

%_Mock.c: %.h
    @echo "Generating mock for $<..."
    $(MOCK_GENERATOR) -o$(dir $@) $<

# 编译测试
test_%.o: test_%.c $(MOCK_OBJS)
    $(CC) $(CFLAGS) -I$(UNITY_DIR)/src -I$(CMOCK_DIR)/src -c $< -o $@

# 链接并运行
run_tests: $(TEST_OBJS) $(MOCK_OBJS)
    $(CC) $^ -o test_runner
    ./test_runner

个人经验:CMock 生成 Mock 时,需要 Ruby 环境。我第一次用的时候忘了装 Ruby,折腾了半天才发现。建议在项目 README 里写清楚前置依赖。

3.3.2 Google Test 集成到 CMake

C++ 项目用 CMake 集成 Google Test 是最省心的方式。CMake 的 FetchContent 模块可以直接拉取源码。

# CMakeLists.txt 片段 - Google Test 集成
cmake_minimum_required(VERSION 3.14)
project(MyProject)

# 启用测试
enable_testing()

# 拉取 Google Test
include(FetchContent)
FetchContent_Declare(
    googletest
    URL https://github.com/google/googletest/archive/release-1.12.1.tar.gz
)
FetchContent_MakeAvailable(googletest)

# 添加测试可执行文件
add_executable(test_runner
    test/test_module1.cpp
    test/test_module2.cpp
)

# 链接 Google Test
target_link_libraries(test_runner
    GTest::GTest
    GTest::Main
)

# 注册测试
add_test(NAME UnitTests COMMAND test_runner)

注意:嵌入式交叉编译环境下,Google Test 可能无法直接运行在目标板上。我一般会在 PC 端用 Google Test 做算法逻辑验证,在目标板上用 Unity 做硬件相关测试。两者互补,不冲突。

3.4 避坑指南

这些年我踩过的坑,总结成几条:

  • 别在嵌入式目标板上跑 Google Test:它依赖 C++ 异常和 RTTI,很多 MCU 编译器不支持。我曾经试过在 Cortex-M4 上跑,编译都过不了。
  • CMock 生成的文件要加入版本控制:虽然它是自动生成的,但团队成员可能 Ruby 版本不一致,导致生成结果不同。我习惯把生成的 Mock 文件提交到 Git,保证一致性。
  • CUnit 的测试注册很繁琐:每个测试用例都要手动添加到 Suite,代码量翻倍。Unity 的自动注册机制就舒服多了。
  • Google Test 的 ASSERT_ 和 EXPECT_ 区别:ASSERT_ 失败会终止当前测试,EXPECT_ 失败会继续执行。我一般在致命错误用 ASSERT_,非致命错误用 EXPECT_。

我的建议:如果项目刚起步,直接上 Unity + CMock 组合。它轻量、可移植、Mock 自动生成,是嵌入式测试的黄金搭档。等团队成熟了,再考虑引入 Google Test 做 PC 端回归测试。