跳到主要内容

自动化测试、接口设计、风格与文档

本节目标

查询 C 测试策略、接口契约、代码风格和 API 文档。

可测试的 C 项目不只是在实现完成后补几条断言。测试要以公开接口为边界,接口要明确调用方与实现方各自负责什么,风格和文档则让这些约定能被稳定审阅。本章使用文本统计库的公开头文件和标准 C 测试程序,说明如何把这些约定留在源码、测试和读者可重放的输出中。构建产物和公开头文件的组织见多文件项目、编译链接与构建系统;诊断工具如何帮助定位失败见警告、调试、静态分析与消毒器

C 测试策略地图

测试层次取决于被验证的接口边界,而不是测试文件所在目录。单元测试聚焦一个函数或模块可观察的结果;集成测试检查多个翻译单元、库或资源边界能否共同工作;命令行行为测试则从进程参数、标准输出、标准错误和退出状态观察整个程序。编译器、调试器和消毒器提供的证据可帮助缩小失败位置,但并不替代这些行为断言。

测试层次目标失败定位
单元测试text_metrics_measure 的结果和状态码输入分类、返回值或输出结构的契约
集成测试实现与公开头文件能否一起编译和链接翻译单元、声明或链接边界
命令行行为测试参数、输出与进程退出状态应用入口与调用方可见行为

每层都应检查调用方真正依赖的结果。只通过内部变量或私有辅助函数的形状来断言,会把测试绑到实现而不是接口;实现重构后即使行为未变也会制造无效失败。

标准 C 最小测试夹具

标准 C 最小测试夹具不需要框架:为每个检查保留名称、比较期望和实际值、累计失败数,并在最后用进程终止状态报告结果。ISO C 只保证 0EXIT_SUCCESS 表示成功、EXIT_FAILURE 表示失败;字面值 1 的含义由宿主环境以实现定义的方式解释。本项目夹具的 return 1 因此是当前宿主和测试运行器的失败约定;若要使用 ISO C 保证的可移植失败状态,应返回 EXIT_FAILURE。一处失败不应阻止独立的后续检查继续提供诊断信息。下面的测试程序使用 fprintf 写失败详情,只有全部通过才输出稳定摘要。

test_text_metrics.c
#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 safely22 字节、1 行、4 个词
空字符串""零字节、零行、零词
连续空白 alpha\tbeta 空白不形成单词
含末尾换行的多行文本alpha\nbeta\n11 字节、2 行、2 个词
非 ASCII 字节\xC3\xA9按 2 个字节、1 个词计数
空文本指针text == NULLTEXT_METRICS_INVALID_ARGUMENT
空结果指针result == NULLTEXT_METRICS_INVALID_ARGUMENT

普通文本没有末尾换行,多行样例带末尾换行,二者共同覆盖此实现的行计数边界。错误路径的状态语义应与库接口一致,不能用未初始化结果或崩溃来代替可检查的错误报告;返回值和哨兵的基础约定见库映射、错误处理与控制流

确定性、隔离与清理

测试不得依赖执行顺序或共享可变状态。五次 expect_metrics 调用分别构造输入,并在辅助函数内使用局部 actual 结果对象;两项错误检查则复用 mainresult 或传入空结果指针。失败计数只由测试程序集中维护;任意一项失败都不会改变其他输入的语义。若测试需要文件、环境变量、临时目录或网络资源,应为每次运行创建独立资源,并在成功与失败路径都清理它们。

固定输入、稳定输出和明确检查的进程状态让问题可重放;时间、随机数、外部服务和宿主路径会破坏这种确定性,除非它们被显式控制或替换。涉及文件打开和资源生命周期时,也要按格式化 I/O、流与文件的关闭与错误处理边界清理。

公开头文件与 API 契约

公开头文件必须能被调用方独立包含。text_metrics.h 自己包含 size_t 所需的 <stddef.h>,声明状态码、结果结构和函数,不要求调用方先包含某个实现专用头。它是调用方可编译、可审阅的 API 契约,而不是把实现细节复制出去的地方。

text_metrics.h
#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 的实际约定
前置条件textresult 都必须不是空指针;text 指向以空字符结束的可读字符串
后置条件成功时将字节、行和词的计数写入调用方提供的 struct text_metrics
错误任一指针为空时返回 TEXT_METRICS_INVALID_ARGUMENT;成功时返回 TEXT_METRICS_OK
所有权函数借用只读的 text,不分配、不释放也不保留它;result 的存储始终由调用方拥有

头文件没有缓冲区参数:结果写入固定大小的调用方结构,而非向调用方缓冲区复制字符。因此这里不能虚构“容量不足”的状态码;API 若接收缓冲区,必须把缓冲区长度和写入规则纳入同一张契约表。

状态码、所有权与缓冲区接口

状态码、所有权和缓冲区长度是 C 接口中必须写明的三个独立问题。状态码让调用方在继续读取输出前判断调用是否成功;所有权说明谁负责保存或释放对象;缓冲区长度说明实现最多能读取或写入多少个元素或字节,并且必须明确计数单位。不能以“调用方应该知道”为由省略其中任何一项。

text_metrics_measure 的状态码枚举只区分成功和无效参数。它不接收调用方缓冲区长度,因为没有字符缓冲区写入接口;它借用输入文本并填充一个调用方所有的结构。动态对象的借用、转移与释放规则可参照动态内存与生命周期

内部实现与链接可见性

公开函数和类型应只暴露调用方需要的名字,其他实现细节留在实现文件。内部函数使用 static 限制链接可见性,使辅助函数不会变成其他翻译单元可依赖的外部符号;测试夹具中的 expect_metricsexpect_status 正是只服务本测试程序的辅助函数。实现以后可以替换这些细节,而不改变公开头文件的契约。

不要把私有函数声明塞进公开头文件,也不要让测试直接依赖它们。需要理解 static 与外部链接的语言规则时,见作用域、链接与存储期

命名、格式与 const 使用

命名和格式规则服务于审阅与接口一致性。项目应为函数、类型、枚举常量和文件名选用可预测的规则,并在同一项目内固定缩进、空格、行长与行末花括号风格;规则本身可以不同,但不能让同一接口在不同文件中看起来像不同约定。

  • text_metrics_measure 这类动词短语命名操作,用 struct text_metrics 表示结果数据。
  • 对不会通过该指针修改的输入使用 const,例如公开接口中的 const char *text;它表达该访问路径不修改文本,不转移所有权。
  • 让控制语句和函数定义使用一致的行末花括号,并让多行条件保持清晰的缩进。
  • 让错误处理与成功路径同样易读,避免通过紧凑写法隐藏状态码检查。

const 的限制范围、可修改左值与对象语义见限定符、对象模型与行为边界

API 注释与使用示例

API 文档必须说明参数、返回值、错误和所有权。对有缓冲区的接口,还应明确缓冲区长度、写入上限、是否写入终止符以及长度单位;对可选参数或回调,应说明允许的空指针和调用时机。注释不应重复函数名,而应补充类型声明无法表达的契约。

text_metrics_measure 的最小用法是:调用方准备自己的结果结构,传入仍有效的只读文本,检查返回的状态码,且只在成功后读取计数。完整的头文件和测试程序是比伪造片段更可靠的可编译示例;若接口改变,应同时更新声明、实现、测试和文档,而不是让其中任一处独自成为真相。

测试框架与 CI 入口

第三方框架和 CI 是扩展入口,不改变测试分层。标准 C 测试程序已经定义了可重放的主线:构建测试可执行文件、运行它、按明确约定检查进程状态和稳定摘要。项目规模、报告需求或平台矩阵增加时,再选择能减少重复工作的工具。

需求可考虑的扩展仍需保留的边界
大量断言、夹具和参数化用例第三方测试框架单元测试仍围绕公开函数的行为
多平台或多编译器重复运行CI 服务每个任务仍检查构建和进程退出状态
覆盖率或报告汇总覆盖率与报告工具覆盖率数字不能证明错误路径已正确断言
外部依赖的端到端验证隔离环境和服务替身明确哪些输入与资源由测试控制

工具的配置语法、供应商运行器和徽章不属于本章的可移植示例;先确定测试层次与接口契约,再按项目约束选工具。

常见测试与接口陷阱

陷阱后果修正方向
只测成功路径空指针、空输入和错误状态在发布后才暴露为正常、边界与错误路径各选代表输入
测试共享状态或执行顺序单独运行、并行运行或重试时得到不同结果隔离输入、资源和清理动作
未说明所有权或缓冲区长度调用方猜测释放责任或读写范围在头文件和文档中写明责任与长度单位
把私有实现当作测试接口重构时产生无意义失败断言公开 API、命令行行为和可见状态
只看测试输出、不看退出状态失败可能被脚本误判为成功保留明确的失败状态约定并检查它

测试、接口、风格和文档相互约束:测试让契约可执行,公开头文件让契约可调用,风格让契约可审阅,文档让契约在源码之外仍可理解。它们共同降低修改实现时破坏调用方的风险。