实战项目一:命令行工具测试
好,我们终于到了第一个实战项目。说实话,很多初学者学单元测试,理论背得滚瓜烂熟,一上手就懵。我个人觉得,最好的学习方式就是直接拿一个真实项目开刀。今天我们就拿一个命令行工具来练手,把之前学的测试套件、覆盖率这些概念,全部用上。
项目背景:一个简易计算器
我们要测试的是一个命令行计算器。它支持加减乘除,还能处理表达式。嗯,听起来简单,但坑不少。我在项目中遇到过类似工具,用户输入"5/0"直接导致程序崩溃,那场面...所以测试必须覆盖边界情况。
先看看这个计算器的核心代码:
// calculator.h
#ifndef CALCULATOR_H
#define CALCULATOR_H
typedef struct {
double result;
int error_code; // 0: 正常, 1: 除零错误, 2: 无效表达式
} calc_result_t;
calc_result_t calc_evaluate(const char* expression);
#endif
// calculator.c
#include "calculator.h"
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
calc_result_t calc_evaluate(const char* expression) {
calc_result_t res = {0.0, 0};
double a, b;
char op;
// 解析 "a op b" 格式
if (sscanf(expression, "%lf %c %lf", &a, &op, &b) != 3) {
res.error_code = 2; // 无效表达式
return res;
}
switch (op) {
case '+': res.result = a + b; break;
case '-': res.result = a - b; break;
case '*': res.result = a * b; break;
case '/':
if (b == 0.0) {
res.error_code = 1; // 除零错误
} else {
res.result = a / b;
}
break;
default:
res.error_code = 2; // 无效操作符
break;
}
return res;
}
搭建完整测试套件
测试套件说白了,就是把所有测试用例组织起来,统一运行。我习惯用宏来写测试框架,轻量又灵活。你想想看,如果每个测试都要手动调用、手动检查结果,那得多累?
这是我的测试框架头文件:
// test_framework.h
#ifndef TEST_FRAMEWORK_H
#define TEST_FRAMEWORK_H
#include <stdio.h>
#include <string.h>
#define TEST(name) void test_##name()
#define RUN_TEST(name) do { \
printf("运行测试: %s\n", #name); \
test_##name(); \
printf(" ✓ 通过\n"); \
} while(0)
#define ASSERT_EQ(actual, expected) do { \
if ((actual) != (expected)) { \
printf(" ✗ 失败: 期望 %d, 实际 %d, 文件 %s, 行 %d\n", \
(int)(expected), (int)(actual), __FILE__, __LINE__); \
return; \
} \
} while(0)
#define ASSERT_DOUBLE_EQ(actual, expected, epsilon) do { \
double diff = (actual) - (expected); \
if (diff < 0) diff = -diff; \
if (diff > (epsilon)) { \
printf(" ✗ 失败: 期望 %f, 实际 %f, 文件 %s, 行 %d\n", \
(double)(expected), (double)(actual), __FILE__, __LINE__); \
return; \
} \
} while(0)
#endif
个人经验:ASSERT_DOUBLE_EQ 这个宏是我踩过坑才加的。浮点数比较直接用 == ?那是在给自己挖坑。0.1 + 0.2 不等于 0.3 这种经典问题,你应该也听说过。
编写测试用例
好了,框架有了,开始写测试。我建议按照功能模块来组织测试文件。每个测试函数只测一个功能点,这样出问题了能快速定位。
// test_calculator.c
#include "test_framework.h"
#include "calculator.h"
// 测试正常加法
TEST(addition_normal) {
calc_result_t res = calc_evaluate("3.5 + 2.5");
ASSERT_EQ(res.error_code, 0);
ASSERT_DOUBLE_EQ(res.result, 6.0, 0.0001);
}
// 测试正常除法
TEST(division_normal) {
calc_result_t res = calc_evaluate("10 / 3");
ASSERT_EQ(res.error_code, 0);
ASSERT_DOUBLE_EQ(res.result, 3.3333, 0.0001);
}
// 测试除零错误
TEST(division_by_zero) {
calc_result_t res = calc_evaluate("5 / 0");
ASSERT_EQ(res.error_code, 1); // 期望除零错误
}
// 测试无效表达式
TEST(invalid_expression) {
calc_result_t res = calc_evaluate("hello world");
ASSERT_EQ(res.error_code, 2); // 期望无效表达式
}
// 测试负数运算
TEST(negative_numbers) {
calc_result_t res = calc_evaluate("-5 + 3");
ASSERT_EQ(res.error_code, 0);
ASSERT_DOUBLE_EQ(res.result, -2.0, 0.0001);
}
// 测试大数运算
TEST(large_numbers) {
calc_result_t res = calc_evaluate("1e10 * 2");
ASSERT_EQ(res.error_code, 0);
ASSERT_DOUBLE_EQ(res.result, 2e10, 0.0001);
}
int main() {
printf("=== 命令行计算器测试套件 ===\n\n");
RUN_TEST(addition_normal);
RUN_TEST(division_normal);
RUN_TEST(division_by_zero);
RUN_TEST(invalid_expression);
RUN_TEST(negative_numbers);
RUN_TEST(large_numbers);
printf("\n所有测试完成!\n");
return 0;
}
覆盖率分析
测试写完了,但怎么知道测全了没有?这就轮到覆盖率工具上场了。我用 gcov 配合 gcc 来做覆盖率分析。具体步骤很简单:
- 编译时加
-fprofile-arcs -ftest-coverage选项 - 运行测试程序
- 用
gcov命令生成报告
编译命令:
gcc -fprofile-arcs -ftest-coverage calculator.c test_calculator.c -o test_calc
运行测试:
./test_calc
生成覆盖率报告:
gcov calculator.c
你会看到类似这样的输出:
File 'calculator.c'
Lines executed: 100.00% of 20
No branches that were not taken
calculator.c:creating 'calculator.c.gcov'
关键指标:行覆盖率 100%,分支覆盖率 100%。这意味着每行代码都被执行过,每个 if-else 分支都走到了。这才是真正的「全覆盖」。
覆盖率达标的标准
你可能要问:覆盖率到底要多少才算达标?我个人经验是这样的:
| 覆盖率类型 | 最低要求 | 推荐目标 | 说明 |
|---|---|---|---|
| 行覆盖率 | 80% | 90%+ | 每行代码至少执行一次 |
| 分支覆盖率 | 70% | 85%+ | 每个 if/else、switch case 都走到 |
| 函数覆盖率 | 90% | 100% | 每个函数都被调用过 |
避坑指南:我曾经见过一个项目,行覆盖率 95%,但线上还是出了严重 bug。为什么?因为那 5% 没覆盖到的代码,恰好是异常处理逻辑。所以别迷信数字,覆盖率只是手段,不是目的。
测试套件运行结果
跑一下我们的测试套件,看看效果:
=== 命令行计算器测试套件 ===
运行测试: addition_normal
✓ 通过
运行测试: division_normal
✓ 通过
运行测试: division_by_zero
✓ 通过
运行测试: invalid_expression
✓ 通过
运行测试: negative_numbers
✓ 通过
运行测试: large_numbers
✓ 通过
所有测试完成!
6 个测试全部通过。配合覆盖率分析,我们确认了 calculator.c 的每一行代码都被测试到了。这就是一个完整的测试闭环。
核心知识体系
我把这个项目的核心逻辑画了张图,方便你理解整个测试流程:
这张图把整个测试流程串起来了。从被测代码开始,经过测试框架、测试用例,最后到运行和报告。每一步都环环相扣,缺一不可。
好了,这个实战项目就到这里。你把这个计算器的测试套件跑一遍,覆盖率分析也做一遍,基本上就掌握了命令行工具测试的完整套路。记住,测试不是写完了就完事,要持续维护、持续补充用例。代码改了,测试也得跟着改,这才是好习惯。
公众号:蓝海资料掘金营,微信deep3321