第十四章:单元测试与Makefile:集成测试到构建系统、自动化测试流程
说实话,很多嵌入式工程师写单元测试,都是手动敲命令跑一跑。项目小还好说,一旦模块多了,几十个测试文件,你一个个编译、链接、运行?那效率太低了。我早年在一个车载项目上吃过这个亏——测试用例写了三百多个,结果每次改完代码,光跑测试就要花半小时手动操作。后来我痛定思痛,把测试集成到了Makefile里。嗯,今天我们就聊聊这个。
为什么要把测试塞进构建系统?
你想想看,构建系统(Makefile)本来就是用来管理编译流程的。测试本质上也是一段可执行程序,只不过它的“输出”是 PASS/FAIL。那为什么不把测试当成一个特殊的 target 来管理呢?
这样做有三个好处:
- 一键执行:make test 搞定所有,不用记一堆命令
- 持续集成:CI 脚本里直接调用 make test,自动化程度拉满
- 回归保护:每次构建时自动跑测试,防止改出 bug
核心思想:让测试成为构建流程中不可跳过的一环,而不是事后补丁。
Makefile 集成测试的基本结构
我们先看一个最简的 Makefile 结构。假设你的项目目录是这样的:
project/
├── src/ # 源代码
├── inc/ # 头文件
├── test/ # 测试用例
│ ├── test_math.c
│ └── test_utils.c
└── Makefile
我习惯在 Makefile 里单独定义一个 test target。它负责编译所有测试文件,链接被测模块,然后运行。来看代码:
CC = gcc
CFLAGS = -Iinc -Wall -Wextra -g
LDFLAGS = -lcmocka # 假设我们用 cmocka 框架
SRC = $(wildcard src/*.c)
TEST_SRC = $(wildcard test/*.c)
TEST_BINS = $(TEST_SRC:.c=)
.PHONY: all test clean
all: test
test: $(TEST_BINS)
@echo "Running all tests..."
@for bin in $^; do \
echo "--- Running $$bin ---"; \
./$$bin || exit 1; \
done
@echo "All tests passed!"
test/test_math: test/test_math.c $(filter-out src/main.c, $(SRC))
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
test/test_utils: test/test_utils.c $(filter-out src/main.c, $(SRC))
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
clean:
rm -f test/test_math test/test_utils
这里有个细节:$(filter-out src/main.c, $(SRC))。为什么要过滤掉 main.c?因为测试程序有自己的 main 函数(由测试框架提供),你不能链接两个 main 进去。我在项目中遇到过有人忘了这茬,链接时报重复定义,排查了半天……
自动化测试流程:从编译到报告
光能跑还不够,我们还要让流程自动化。一个完整的自动化测试流程应该包含:
- 编译测试目标:确保测试代码本身没有语法错误
- 链接被测模块:把要测的 .c 文件(除了 main)链接进来
- 执行测试:运行所有测试二进制文件
- 收集结果:判断 PASS/FAIL,生成汇总
- 失败即停止:任何一个测试失败,整个构建就失败
我曾经在一个项目中,测试脚本里用了 || true 来忽略错误。结果呢?测试红了,CI 还是绿的。上线后才发现功能被破坏了。所以记住:测试失败必须让 make 返回非零值。
警告:不要在测试命令后面加 || true 或 set +e。测试失败就应该让构建失败,否则自动化就失去了意义。
进阶技巧:并行测试与覆盖率
当测试用例多起来,串行跑会很慢。Makefile 天然支持 -j 并行,但测试之间如果有共享资源(比如写同一个文件),并行就会出问题。我的做法是:每个测试用例使用独立的临时目录,或者用 flock 加锁。
另外,覆盖率也是自动化测试的重要一环。我们可以用 gcov/lcov 来生成覆盖率报告:
COVERAGE_FLAGS = -fprofile-arcs -ftest-coverage
test_coverage: CFLAGS += $(COVERAGE_FLAGS)
test_coverage: test
@echo "Generating coverage report..."
@lcov --capture --directory . --output-file coverage.info
@genhtml coverage.info --output-directory coverage_report
@echo "Coverage report at coverage_report/index.html"
这样,你跑 make test_coverage,就能看到哪些代码被测试覆盖了,哪些没覆盖到。嗯,这个在代码评审时特别有用。
避坑指南:我踩过的几个坑
- 坑一:测试文件依赖没写全。改了 src 里的代码,make 不知道要重新编译测试。解决方案:用
gcc -MM自动生成依赖。 - 坑二:测试二进制文件没清理。旧版本的测试二进制还在,跑的是老代码。我建议
make clean时把测试二进制也删掉。 - 坑三:测试框架的链接顺序。有些链接器对库的顺序敏感,cmocka 的库要放在最后。我曾经被这个坑了整整一个下午。
小技巧:在 Makefile 里加一个 make test_verbose target,输出每个测试的详细信息。调试时特别管用。
知识体系图:测试集成到构建系统
下面这张图展示了测试集成到 Makefile 后的完整流程。你可以看到,从代码修改到测试报告,整个链路是自动化的。
总结一下
把单元测试集成到 Makefile 里,说白了就是让机器替你干活。你只需要写一次规则,之后每次改代码,跑一下 make test,所有测试自动编译、链接、运行、出报告。我个人觉得,这是嵌入式项目从“野路子”走向“正规军”的第一步。
嗯,最后提醒一句:别把测试当成负担。它其实是你的安全网。网织得越密,你改代码时就越敢下手。
一句话记住:测试不集成到构建系统,等于没写测试。
公众号:蓝海资料掘金营,微信 deep3321