实战项目一:命令行工具测试

好,我们终于到了第一个实战项目。说实话,很多初学者学单元测试,理论背得滚瓜烂熟,一上手就懵。我个人觉得,最好的学习方式就是直接拿一个真实项目开刀。今天我们就拿一个命令行工具来练手,把之前学的测试套件、覆盖率这些概念,全部用上。

项目背景:一个简易计算器

我们要测试的是一个命令行计算器。它支持加减乘除,还能处理表达式。嗯,听起来简单,但坑不少。我在项目中遇到过类似工具,用户输入"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 来做覆盖率分析。具体步骤很简单:

  1. 编译时加 -fprofile-arcs -ftest-coverage 选项
  2. 运行测试程序
  3. 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 的每一行代码都被测试到了。这就是一个完整的测试闭环。

核心知识体系

我把这个项目的核心逻辑画了张图,方便你理解整个测试流程:

命令行工具测试完整流程 被测代码 (calculator.c) 测试框架 (test_framework.h) 测试用例 (test_calculator.c) 运行测试 → 生成覆盖率报告 测试通过 / 覆盖率达标 关键要点 • 测试套件统一管理用例 • 每个测试函数只测一个点 • 浮点数比较用 epsilon • 覆盖率目标:行90%+ • 分支覆盖率85%+ • 函数覆盖率100% • 用 gcov 分析覆盖率 • 不要迷信数字 • 异常路径必须覆盖 • 测试框架轻量灵活 • 宏定义简化测试编写 • 自动化运行是关键

这张图把整个测试流程串起来了。从被测代码开始,经过测试框架、测试用例,最后到运行和报告。每一步都环环相扣,缺一不可。

好了,这个实战项目就到这里。你把这个计算器的测试套件跑一遍,覆盖率分析也做一遍,基本上就掌握了命令行工具测试的完整套路。记住,测试不是写完了就完事,要持续维护、持续补充用例。代码改了,测试也得跟着改,这才是好习惯。


公众号:蓝海资料掘金营,微信deep3321