自动化测试、接口设计、风格与文档
本节目标
查询 C 测试策略、接口契约、代码风格和 API 文档。
可测试的 C 项目不只是在实现完成后补几条断言。测试要以公开接口为边界,接口要明确调用方与实现方各自负责什么,风格和文档则让这些约定能被稳定审阅。本章使用文本统计库的公开头文件和标准 C 测试程序,说明如何把这些约定留在源码、测试和读者可重放的输出中。构建产物和公开头文件的组织见多文件项目、编译链接与构建系统;诊断工具如何帮助定位失败见警告、调试、静态分析与消毒器。
C 测试策略地图
测试层次取决于被验证的接口边界,而不是测试文件所在目录。单元测试聚焦一个函数或模块可观察的结果;集成测试检查多个翻译单元、库或资源边界能否共同工作;命令行行为测试则从进程参数、标准输出、标准错误和退出状态观察整个程序。编译器、调试器和消毒器提供的证据可帮助缩小失败位置,但并不替代这些行为断言。
| 测试层次 | 目标 | 失败定位 |
|---|---|---|
| 单元测试 | text_metrics_measure 的结果和状态码 | 输入分类、返回值或输出结构的契约 |
| 集成测试 | 实现与公开头文件能否一起编译和链接 | 翻译单元、声明或链接边界 |
| 命令行行为测试 | 参数、输出与进程退出状态 | 应用入口与调用方可见行为 |
每层都应检查调用方真正依赖的结果。只通过内部变量或私有辅助函数的形状来断言,会把测试绑到实现而不是接口;实现重构后即使行为未变也会制造无效失败。
标准 C 最小测试夹具
标准 C 最小测试夹具不需要框架:为每个检查保留名称、比较期望和实际值、累计失败数,并在最后用进程终止状态报告结果。ISO C 只保证 0 或 EXIT_SUCCESS 表示成功、EXIT_FAILURE 表示失败;字面值 1 的含义由宿主环境以实现定义的方式解释。本项目夹具的 return 1 因此是当前宿主和测试运行器的失败约定;若要使用 ISO C 保证的可移植失败状态,应返回 EXIT_FAILURE。一处失败不应阻止独立的后续检查继续提供诊断信息。下面的测试程序使用 fprintf 写失败详情,只有全部通过才输出稳定摘要。
#include "text_metrics.h"
#include <stdio.h>
static int failures;
static void expect_metrics(
const char *name,
const char *text,
size_t expected_bytes,
size_t expected_lines,
size_t expected_words
) {
struct text_metrics actual;
enum text_metrics_status status = text_metrics_measure(text, &actual);
if (status != TEXT_METRICS_OK) {
fprintf(
stderr,
"%s: expected status=%d, received status=%d\n",
name,
TEXT_METRICS_OK,
status
);
++failures;
return;
}
if (actual.bytes != expected_bytes || actual.lines != expected_lines ||
actual.words != expected_words) {
fprintf(
stderr,
"%s: expected bytes=%zu lines=%zu words=%zu, received bytes=%zu lines=%zu words=%zu\n",
name,
expected_bytes,
expected_lines,
expected_words,
actual.bytes,
actual.lines,
actual.words
);
++failures;
}
}
static void expect_status(
const char *name,
enum text_metrics_status actual,
enum text_metrics_status expected
) {
if (actual != expected) {
fprintf(
stderr,
"%s: expected status=%d, received status=%d\n",
name,
expected,
actual
);
++failures;
}
}
int main(void) {
struct text_metrics result;
expect_metrics("fixed", "C23 tools build safely", 22, 1, 4);
expect_metrics("empty", "", 0, 0, 0);
expect_metrics("whitespace", " alpha\tbeta ", 14, 1, 2);
expect_metrics("multiline", "alpha\nbeta\n", 11, 2, 2);
expect_metrics("non-ascii-bytes", "\xC3\xA9", 2, 1, 1);
expect_status(
"null-text",
text_metrics_measure(NULL, &result),
TEXT_METRICS_INVALID_ARGUMENT
);
expect_status(
"null-result",
text_metrics_measure("text", NULL),
TEXT_METRICS_INVALID_ARGUMENT
);
if (failures != 0) {
return 1;
}
fputs("tests=7 passed\n", stdout);
return 0;
}
tests=7 passed
这里的输出是成功摘要,不是把测试成功建立在输出字符串解析上。自动化调用应同时检查进程退出状态;失败时测试程序保留测试名、期望值和实际值,便于直接定位。
单元、集成与命令行测试
同一项目可以组合单元测试、集成测试和命令行行为测试,而不是在它们之间三选一。test_text_metrics.c 直接调用公开函数,是以函数契约为中心的单元测试;将它与库实现编译并运行,同时覆盖了头文件、实现和链接的集成边界;命令行程序还应单独验证参数缺失时的错误信息和退出状态。
测试不应跳过失败路径:命令行行为测试应观察进程状态和可见输出,而不是只确认某次普通参数运行成功。关于编译期、链接期和运行期证据的分工,回看诊断工具选择地图。
正常、边界与错误路径
测试数据按正常、边界与错误路径分类,才能避免只测最常见的成功调用。text_metrics 的七项检查没有把编码当作字符数:它统计 C 字符串终止符前的字节,并把空白划分为单词边界。
| 输入分类 | 夹具中的代表输入 | 要锁定的结果 |
|---|---|---|
| 固定正常文本 | C23 tools build safely | 22 字节、1 行、4 个词 |
| 空字符串 | "" | 零字节、零行、零词 |
| 连续空白 | alpha\tbeta | 空白不形成单词 |
| 含末尾换行的多行文本 | alpha\nbeta\n | 11 字节、2 行、2 个词 |
| 非 ASCII 字节 | \xC3\xA9 | 按 2 个字节、1 个词计数 |
| 空文本指针 | text == NULL | TEXT_METRICS_INVALID_ARGUMENT |
| 空结果指针 | result == NULL | TEXT_METRICS_INVALID_ARGUMENT |
普通文本没有末尾换行,多行样例带末尾换行,二者共同覆盖此实现的行计数边界。错误路径的状态语义应与库接口一致,不能用未初始化结果或崩溃来代替可检查的错误报告;返回值和哨兵的基础约定见库映射、错误处理与控制流。
确定性、隔离与清理
测试不得依赖执行顺序或共享可变状态。五次 expect_metrics 调用分别构造输入,并在辅助函数内使用局部 actual 结果对象;两项错误检查则复用 main 的 result 或传入空结果指针。失败计数只由测试程序集中维护;任意一项失败都不会改变其他输入的语义。若测试需要文件、环境变量、临时目录或网络资源,应为每次运行创建独立资源,并在成功与失败路径都清理它们。
固定输入、稳定输出和明确检查的进程状态让问题可重放;时间、随机数、外部服务和宿主路径会破坏这种确定性,除非它们被显式控制或替换。涉及文件打开和资源生命周期时,也要按格式化 I/O、流与文件的关闭与错误处理边界清理。
公开头文件与 API 契约
公开头文件必须能被调用方独立包含。text_metrics.h 自己包含 size_t 所需的 <stddef.h>,声明状态码、结果结构和函数,不要求调用方先包含某个实现专用头。它是调用方可编译、可审阅的 API 契约,而不是把实现细节复制出去的地方。
#ifndef TEXT_METRICS_H
#define TEXT_METRICS_H
#include <stddef.h>
enum text_metrics_status {
TEXT_METRICS_OK = 0,
TEXT_METRICS_INVALID_ARGUMENT
};
struct text_metrics {
size_t bytes;
size_t lines;
size_t words;
};
enum text_metrics_status text_metrics_measure(
const char *text,
struct text_metrics *result
);
#endif
| 契约部分 | text_metrics_measure 的实际约定 |
|---|---|
| 前置条件 | text 与 result 都必须不是空指针;text 指向以空字符结束的可读字符串 |
| 后置条件 | 成功时将字节、行和词的计数写入调用方提供的 struct text_metrics |
| 错误 | 任一指针为空时返回 TEXT_METRICS_INVALID_ARGUMENT;成功时返回 TEXT_METRICS_OK |
| 所有权 | 函数借用只读的 text,不分配、不释放也不保留它;result 的存储始终由调用方拥有 |
头文件没有缓冲区参数:结果写入固定大小的调用方结构,而非向调用方缓冲区复制字符。因此这里不能虚构“容量不足”的状态码;API 若接收缓冲区,必须把缓冲区长度和写入规则纳入同一张契约表。
状态码、所有权与缓冲区接口
状态码、所有权和缓冲区长度是 C 接口中必须写明的三个独立问题。状态码让调用方在继续读取输出前判断调用是否成功;所有权说明谁负责保存或释放对象;缓冲区长度说明实现最多能读取或写入多少个元素或字节,并且必须明确计数单位。不能以“调用方应该知道”为由省略其中任何一项。
text_metrics_measure 的状态码枚举只区分成功和无效参数。它不接收调用方缓冲区长度,因为没有字符缓冲区写入接口;它借用输入文本并填充一个调用方所有的结构。动态对象的借用、转移与释放规则可参照动态内存与生命周期。
内部实现与链接可见性
公开函数和类型应只暴露调用方需要的名字,其他实现细节留在实现文件。内部函数使用 static 限制链接可见性,使辅助函数不会变成其他翻译单元可依赖的外部符号;测试夹具中的 expect_metrics 和 expect_status 正是只服务本测试程序的辅助函数。实现以后可以替换这些细节,而不改变公开头文件的契约。
不要把私有函数声明塞进公开头文件,也不要让测试直接依赖它们。需要理解 static 与外部链接的语言规则时,见作用域、链接与存储期。
命名、格式与 const 使用
命名和格式规则服务于审阅与接口一致性。项目应为函数、类型、枚举常量和文件名选用可预测的规则,并在同一项目内固定缩进、空格、行长与行末花括号风格;规则本身可以不同,但不能让同一接口在不同文件中看起来像不同约定。
- 用
text_metrics_measure这类动词短语命名操作,用struct text_metrics表示结果数据。 - 对不会通过该指针修改的输入使用
const,例如公开接口中的const char *text;它表达该访问路径不修改文本,不转移所有权。 - 让控制语句和函数定义使用一致的行末花括号,并让多行条件保持清晰的缩进。
- 让错误处理与成功路径同样易读,避免通过紧凑写法隐藏状态码检查。
const 的限制范围、可修改左值与对象语义见限定符、对象模型与行为边界。
API 注释与使用示例
API 文档必须说明参数、返回值、错误和所有权。对有缓冲区的接口,还应明确缓冲区长度、写入上限、是否写入终止符以及长度单位;对可选参数或回调,应说明允许的空指针和调用时机。注释不应重复函数名,而应补充类型声明无法表达的契约。
text_metrics_measure 的最小用法是:调用方准备自己的结果结构,传入仍有效的只读文本,检查返回的状态码,且只在成功后读取计数。完整的头文件和测试程序是比伪造片段更可靠的可编译示例;若接口改变,应同时更新声明、实现、测试和文档,而不是让其中任一处独自成为真相。
测试框架与 CI 入口
第三方框架和 CI 是扩展入口,不改变测试分层。标准 C 测试程序已经定义了可重放的主线:构建测试可执行文件、运行它、按明确约定检查进程状态和稳定摘要。项目规模、报告需求或平台矩阵增加时,再选择能减少重复工作的工具。
| 需求 | 可考虑的扩展 | 仍需保留的边界 |
|---|---|---|
| 大量断言、夹具和参数化用例 | 第三方测试框架 | 单元测试仍围绕公开函数的行为 |
| 多平台或多编译器重复运行 | CI 服务 | 每个任务仍检查构建和进程退出状态 |
| 覆盖率或报告汇总 | 覆盖率与报告工具 | 覆盖率数字不能证明错误路径已正确断言 |
| 外部依赖的端到端验证 | 隔离环境和服务替身 | 明确哪些输入与资源由测试控制 |
工具的配置语法、供应商运行器和徽章不属于本章的可移植示例;先确定测试层次与接口契约,再按项目约束选工具。
常见测试与接口陷阱
| 陷阱 | 后果 | 修正方向 |
|---|---|---|
| 只测成功路径 | 空指针、空输入和错误状态在发布后才暴露 | 为正常、边界与错误路径各选代表输入 |
| 测试共享状态或执行顺序 | 单独运行、并行运行或重试时得到不同结果 | 隔离输入、资源和清理动作 |
| 未说明所有权或缓冲区长度 | 调用方猜测释放责任或读写范围 | 在头文件和文档中写明责任与长度单位 |
| 把私有实现当作测试接口 | 重构时产生无意义失败 | 断言公开 API、命令行行为和可见状态 |
| 只看测试输出、不看退出状态 | 失败可能被脚本误判为成功 | 保留明确的失败状态约定并检查它 |
测试、接口、风格和文档相互约束:测试让契约可执行,公开头文件让契约可调用,风格让契约可审阅,文档让契约在源码之外仍可理解。它们共同降低修改实现时破坏调用方的风险。