第28章 代码审查与测试:审查清单、测试覆盖率审查、常见代码缺陷
代码审查这事儿,我做了十几年了。说实话,刚入行那会儿我觉得审查就是浪费时间——自己写的代码自己最清楚,干嘛还要别人看?直到有一次,我提交了一个看似完美的模块,结果被同事揪出一个极其隐蔽的指针越界问题。嗯,从那以后,我再也不敢小看代码审查了。
这一章,咱们就聊聊代码审查和测试覆盖率审查。我会结合自己的经验,给你一份实用的审查清单,再讲讲那些常见的代码缺陷。你想想看,如果能把这些东西内化成习惯,你的代码质量至少能提升一个档次。
28.1 代码审查清单:我自己的“防呆”手册
我个人习惯把审查清单分成几个维度。每次审查代码时,我都会在心里过一遍这张清单。说白了,这就是一份“防呆”手册,防止自己漏掉关键点。
28.1.1 逻辑与算法审查
这是最核心的部分。代码能不能跑对,全看逻辑。
- 边界条件:数组的第一个和最后一个元素处理了吗?循环的起始和终止条件对吗?
- 空值处理:指针是否为NULL?字符串是否为空?结构体是否已初始化?
- 溢出风险:整数运算会不会溢出?我记得有一次,一个计数器在极端情况下溢出了,导致系统死循环。从那以后,我对所有累加操作都格外小心。
- 状态机完整性:所有状态都有对应的处理吗?有没有遗漏的状态转换?
核心原则:永远假设输入是恶意的。哪怕是你自己写的函数,也要假设调用者会传进来各种奇怪的值。
28.1.2 内存与资源管理审查
嵌入式开发中,内存问题是最常见的“隐形杀手”。
- malloc/free 配对:每次分配都有对应的释放吗?有没有内存泄漏?
- 野指针:释放后有没有把指针置为NULL?我曾经见过一个项目,因为野指针导致系统随机崩溃,排查了整整三天。
- 栈溢出:局部变量是不是太大了?递归深度有没有控制?
- 资源泄漏:文件句柄、锁、信号量,这些资源用完后有没有归还?
避坑指南:我曾经在审查一个通信协议栈时,发现某个错误处理路径下忘记释放锁。结果那个锁一旦被占用,整个系统就死锁了。所以,我建议你在写错误处理代码时,先把资源释放逻辑写出来,再写正常逻辑。
28.1.3 并发与同步审查
多线程或多任务环境下,问题往往出在“时机”上。
- 临界区保护:共享变量有没有加锁?锁的粒度是否合理?
- 死锁风险:锁的获取顺序是否一致?有没有嵌套锁?
- 竞态条件:检查-使用模式(check-then-act)是否安全?
- 原子操作:对于简单的读写,是否可以用原子操作代替锁?
28.2 测试覆盖率审查:别被数字骗了
很多团队把“测试覆盖率”当成KPI,觉得只要覆盖率超过80%就万事大吉。其实不然。我见过一个项目,覆盖率高达90%,但核心逻辑的边界条件根本没测到。
测试覆盖率审查,说白了就是检查你的测试到底测了什么、没测什么。
28.2.1 语句覆盖率 vs. 分支覆盖率
这两个概念经常被混淆。我简单解释一下:
| 覆盖率类型 | 含义 | 常见误区 |
|---|---|---|
| 语句覆盖率 | 每行代码是否被执行过 | 容易达到100%,但意义有限 |
| 分支覆盖率 | 每个if-else分支是否都被覆盖 | 比语句覆盖率更有价值 |
| 条件覆盖率 | 每个布尔子表达式是否都取过true/false | 最严格,也最难达到 |
| MC/DC覆盖率 | 每个条件独立影响结果 | 航空、汽车等安全关键领域要求 |
我个人习惯,至少要求分支覆盖率超过85%。对于关键模块,我会要求MC/DC覆盖率。
28.2.2 审查测试用例的“盲区”
审查覆盖率报告时,我会重点关注几个地方:
- 异常路径:错误处理代码有没有被测试覆盖?很多测试只测“快乐路径”,忽略了异常情况。
- 边界值:比如一个函数接受0-100的参数,测试用例有没有包含0、100、-1、101?
- 时序相关:并发场景下的测试是否充分?有没有模拟过超时、中断等场景?
小技巧:我通常会在审查时,手动在覆盖率报告上标记出“未覆盖的代码行”。然后问自己:这些代码真的不需要测吗?还是测试用例遗漏了?
28.3 常见代码缺陷:我踩过的那些坑
下面这些缺陷,是我在多年审查中反复遇到的。你想想看,如果能提前避开这些坑,能省下多少调试时间。
28.3.1 整数溢出与符号问题
这是C语言的老大难问题。我见过一个案例:一个温度传感器读取值,用int8_t存储,结果温度超过127度后直接变成负数,导致系统误判为“低温报警”。
// 错误示例:整数溢出
int16_t calculate_average(int16_t *data, uint8_t len) {
int16_t sum = 0;
for (uint8_t i = 0; i < len; i++) {
sum += data[i]; // 如果len很大,sum可能溢出
}
return sum / len;
}
// 改进:使用更大的类型,或检查溢出
int32_t calculate_average_safe(int16_t *data, uint8_t len) {
int32_t sum = 0;
for (uint8_t i = 0; i < len; i++) {
sum += data[i];
}
return sum / len;
}
28.3.2 缓冲区溢出
这个缺陷在嵌入式开发中尤其致命。因为嵌入式系统通常没有MMU保护,缓冲区溢出直接破坏栈或堆,导致各种诡异的问题。
// 错误示例:未检查长度
void copy_data(char *dest, const char *src) {
strcpy(dest, src); // 如果src比dest长,就溢出了
}
// 改进:使用带长度限制的函数
void copy_data_safe(char *dest, size_t dest_size, const char *src) {
strncpy(dest, src, dest_size - 1);
dest[dest_size - 1] = '\0';
}
避坑指南:我曾经审查过一个网络协议栈,里面用了sprintf拼接日志字符串。结果某个异常情况下,日志内容超长,直接覆盖了相邻的变量。从那以后,我要求团队一律使用snprintf。
28.3.3 未初始化变量
这个缺陷很隐蔽,因为局部变量在栈上的初始值是不确定的。不同编译器、不同优化级别下,行为可能完全不同。
// 错误示例:未初始化
int process_data(void) {
int result; // 未初始化
if (some_condition) {
result = 100;
}
// 如果some_condition为false,result的值是未定义的
return result;
}
// 改进:总是初始化
int process_data_safe(void) {
int result = 0; // 明确初始化
if (some_condition) {
result = 100;
}
return result;
}
28.3.4 逻辑短路与副作用
C语言的逻辑运算符(&&、||)有短路特性。如果滥用,会导致意想不到的行为。
// 错误示例:依赖副作用的短路
if (ptr != NULL && *ptr == 10) {
// 没问题,短路保护了
}
// 危险示例:函数调用有副作用
if (func_a() || func_b()) {
// 如果func_a返回true,func_b不会被执行
// 如果func_b有副作用(比如修改全局变量),就出问题了
}
28.4 知识体系:代码审查与测试的核心逻辑
下面这张图,是我自己总结的代码审查与测试的知识体系。你可以把它当作一张“地图”,随时对照检查。
28.5 我的审查流程:从“看代码”到“读代码”
最后,分享一下我个人的审查流程。说白了,就是怎么把“看代码”变成“读代码”。
- 先看整体结构:不急着看细节。先理解模块的职责、接口、数据流。
- 再走读关键路径:跟着主要逻辑走一遍,确认流程是否正确。
- 然后检查边界:重点关注边界条件、异常处理、错误路径。
- 最后审查细节:变量命名、注释、代码风格等。
个人经验:我每次审查时,都会在代码旁边写注释。不是写“这里错了”,而是写“这里为什么这样写?”。有时候,写着写着就发现问题了。这个方法,我推荐给所有团队成员。
代码审查不是走过场,而是实实在在的质量保障手段。你想想看,如果能通过审查提前发现90%的缺陷,那调试阶段能省下多少时间?
公众号:蓝海资料掘金营,微信deep3321