[SKILL] 单元测试设计规则参考


UNIT_TEST_cn.md
# 单元测试规则

> 适用范围:各被测模块及其配套代码的宿主侧单元测试。本规则是测试编写、审查与验收的
> 依据,自含,不依赖其他文档定义。

---

## 1. 范围与目标

1. 测试在宿主上运行(如经 Unity 等宿主测试框架),不依赖目标硬件。
2. 被测对象为纯算法模块与 OS 中立的接口层。底层总线访问、并发原语、外部物理过程由替身
   (mock)提供。
3. 测试目标是稳健地确定并验证被测代码的行为契约——测试即单元行为的设计说明,而非
   「碰巧观察到的一切」。验证对象是被测代码的输出与契约,不验证外部物理过程的闭环响应。
4. 配置项不属于被测单元(正是为此才把它从单元代码中抽出),不为之写单元测试;对固化在
   代码里的配置特性(如属性、开关、过滤器),应在外部可观察层面用集成测试覆盖,而非
   断言「源码里存在该特性」。
5. 测试遵循 FIRST 原则:快速(Fast)、隔离(Isolated)、可重复(Repeatable)、
   自动判定(Self-validating)、及时(Timely)。以下各章是这些原则在宿主侧的具体落实。

## 2. 组织与构建

1. 构建由各模块的测试构建系统组织,产物为单一宿主二进制。
2. 测试按模块分文件,外加共享替身文件。
3. 统一入口集中持有 `setUp`/`tearDown`/`main`、全部前向声明与测试登记(如 Unity 的
   `RUN_TEST`)。测试函数保持外部链接(非 `static`),以支持跨文件集中登记。
4. 实现文件使用英文注释;测试文件使用中文注释。
5. `setUp` 须重置全部替身状态,保证用例间无残留依赖。
6. 公共 setup 须保持最小:只放「所有用到它的测试都需要其全部前置条件」的内容。禁止让
   一大段公共 setup 在许多互不相关的测试开头都跑一遍——那会让每个测试实际依赖的假设
   变得不清,也说明你测的不止一个单元。少数测试共用同一 setup 仅在它们确实需要完全
   相同的前置条件时才可接受。
7. 测试体按 Arrange(准备)/Act(执行)/Assert(断言)三段组织,以空行分隔;每段单一
   职责,避免 setup 与断言交错。
8. 测试须确定性,禁止依赖随机、墙钟时间、并发竞态、文件系统或网络;需要时间或随机时
   由用例注入固定值。

## 3. 命名约定

1. 测试函数名采用三段式 `TEST_<Lib>_<Subject>_<Scenario>_<Result>`(即「主题 _ 场景 _
   结果」S/S/R 的扩展形式,加 `<Lib>` 作命名空间前缀)。各段以下划线 `_` 分隔,段内单词
   用 PascalCase(每个单词首字母大写、直接连写,不插入下划线)。
   - `<Lib>`:被测库或模块的简写,作为命名空间前缀,避免不同库的测试符号冲突。
   - `<Subject>`:被测函数或概念名。
   - `<Scenario>`:前置条件或输入状态,以介词引导(`On`/`When`/`In`/`With`/`Given`/
     `After`/`During`/`Over`/`At`/`Across` 等,可按需扩展),全库风格一致即可
     (如 `OnEmptyInput`、`WhenDisabled`、`DuringTransfer`、`GivenNullHandle`、`OverFullMove`)。
   - `<Result>`:预期行为或输出(如 `ConvergesNoOvershoot`、`ReturnsParam`、
     `ScalesToMilliamps`)。
2. 命名示例(以通用数据结构为被测对象,仅示意三段式结构):

   ```c
   TEST_Queue_Push_OnFullBuffer_ReturnsFull
   TEST_Queue_Pop_WhenEmpty_ReturnsNull
   TEST_Filter_Sample_OnConstantInput_ConvergesToValue
   TEST_Store_Write_AfterErase_RoundTripsData
   ```

3. 名实一致:名字必须如实描述测试实际动作与断言验证的性质。
   - 禁止名实不符(名字声称测 X,实际测 Y)。
   - 禁止过度承诺(名字承诺某性质而断言只覆盖其子集,如以 `Any`/`All` 命名却只测单一来源、
     以 `OnEveryCall` 命名却只覆盖单一分支)。
   - 禁止模糊(`Result` 段缺失或过弱,如仅写 `Works`/`Ok` 而实际验证了更具体性质,
     应改为承载实际性质的措辞)。
   - 误导性强调(名字含 `Clamps` 但测试中未发生钳位、含 `Symmetric` 但无对照)须改为如实措辞。
4. 命名反例与正例对照(抽象形式,仅说明内在要求):

   | 反例模式 | 正例模式 | 内在要求 |
   |---|---|---|
   | `<Func>_OnValidInput_<X>` 且 `<X>` 为 `Works`/`Ok` | `<Func>_OnValidInput_<具体效果>` | `Result` 段须具体,禁用空泛词 |
   | `<Func>_AllInputs_<X>`(实际只覆盖一类输入)| `<Func>_On<InputA>_<X>` | `Scenario` 须如实反映所测范围,禁 `All`/`Any` 过度承诺 |
   | `<Func>_Clamps<X>` 但测试中未发生钳位 | `<Func>_OnOverflow_ReturnsBound` | 强调动词须与实际行为匹配 |
   | `<Func>_Converges` 但实际还验证了不过冲与单调 | `<Func>_ConvergesNoOvershoot` | `Result` 须承载实际验证的全部性质 |
   | `<Func>_OnKnownX_<Y>` 但输入含非 X 类样本 | `<Func>_AcrossXAndNonX_<Y>` | `Scenario` 描述的输入范围须与实际一致 |

## 4. 覆盖深度:端点与过程

1. 收敛类、轨迹类、时序类测试必须同时覆盖端点与过程,不得仅采样终值。
2. 过程断言的形式:
   - 逐拍不变量:单调性(对前值 `>=`/`<=`)、上下界钳位。循环仅用于重复同性质的不变量
     检查;禁止用条件分支(`if`/`switch`/标志位)决定走哪条断言路径。
   - 相态验证:复合过程须断言各阶段俱在(如加速-巡航-减速、请求-处理-完成)。
   - 中间采样:在过程中段断言精确值,与端点共同刻画轨迹。
3. 过程类被测逻辑须覆盖完整生命周期:建立、稳态、终止、重定目标(反向、同向延伸、跨零)。

## 5. 边界与对称

1. 对称性:凡存在正负、增减、充放等对称语义的输入,必须独立测试两侧,不得以一侧结论
   推断另一侧。符号扩展、负数整数除法截断方向等路径须单独覆盖。
2. 退化与边界配置:零值、极值、溢出边界、空输入、超限参数、未初始化状态须逐项覆盖;
   重点防范符号翻转、负移位等未定义行为回归。
3. 状态/模式切换须分静态与动态:
   - 静态:稳态或空闲状态下切换。
   - 动态:过程进行中切换,验证内部状态接续、无跳变、缓存与计数不被污染。
4. 参数设定后的执行、切换、过渡:设定可调参数后,必须验证后续输出实际受该参数约束
   (如设上限后输出只到上限、设速率后每拍增量匹配、过程进行中改参数立即影响下一拍)。

## 6. 断言严格性

1. 禁止重言式断言:断言与被测逻辑须有因果。若被测代码的常数或运算符被改错时断言仍通过,
   该断言无效。
2. 数值换算测试须选择使变换非退化的参数;禁止选择使换算退化为单位矩阵或恒等映射的取值。
3. 回环测试(write 后 read)当 write 与 read 共用同一映射时构成自洽恒等,无法捕获映射
   错误。须以独立路径(如按字段常量直读底层存储位段)打破回环。
4. 容差须与典型偏差匹配:
   - 禁止过宽(`WITHIN` 容差或范围断言能容纳错误实现)。
   - 禁止装饰性(仅断言 `> 0` 而使参数减半的变异仍通过)。
5. 过紧脆弱断言(依赖实现内部状态或未契约化的 magic number)应改为行为契约断言;若保留
   实现相关常量,须在测试注释中声明该耦合。
6. 鲁棒性测试除验证"不崩溃"外,还须断言无副作用(输出写入计数、并发原语获取计数不变)。
7. 断言须针对被测代码的输出(返回值、状态、写入目标的内容),不得仅断言"替身被怎样调用"——
   后者验证的是替身而非被测逻辑。
8. 一个测试聚焦单一行为;多个不同性质的断言应拆分为多个测试,避免失败时无法定位
   (Assertion Roulette)。同性质的多拍不变量(如逐拍单调)不在此列。
9. 不做多余断言:对别的测试已经断言过的性质再断言一遍有害无益,只会让无意义的失败更
   频繁,对覆盖率毫无帮助;若某次观察不是被测的核心行为,就不要再去断言它。

## 7. 替身(mock)设计

1. 术语按角色区分:Stub 提供假数据(假寄存器/假缓冲区)、Spy 记录调用(写入计数、按地址
   计数、锁获取/释放)、Fake 提供简化实现。宿主测试通常混用 Stub 与 Spy,下文统称"替身"。
2. 替身提供底层存储的假模型与观察计数器,按实例标识分桶以支持多实例。
3. 替身的底层函数不调用并发钩子,以隔离被测代码自身的并发配对行为。
4. 替身无外部过程模型:写入目标不改变实际反馈量;反馈量由用例预置。断言对象是被测代码
   写入的输出,而非外部闭环响应。
5. 替身的简化(如跳过某层换算、不仿真某硬件流水时序)必须在替身头注释中诚实记录,并
   指出由此产生的覆盖盲区。

## 8. 验证流程

1. 新增或修改测试后,构建须无警告且全部用例通过。
2. 关键断言(数值换算、映射/路由、接续性质、边界钳位)须经变异测试验证:临时改变被测
   代码的常数、运算符、地址映射或分支条件,确认对应断言失败,再还原。
3. 命名与断言严格性可由独立审查按本规则第 3、6 章逐条核对,产出名实不符、过度承诺、
   过宽/过紧、重言式、装饰性断言清单。

## 9. 审查清单

测试编写与修改后,按以下清单逐项确认:

- [ ] 01. 未对配置项或固化在代码里的配置特性写单元测试。
- [ ] 02. 测试由模块构建系统构建,无警告、全 PASS。
- [ ] 03. 测试体按 Arrange/Act/Assert 三段组织;测试确定性,无随机/时间/竞态/外部依赖。
- [ ] 04. 公共 setup 最小化,仅含共享测试都需要的全部前置条件;`setUp` 重置全部替身状态。
- [ ] 05. 函数名符合 `TEST_<Lib>_<Subject>_<Scenario>_<Result>`,统一入口的前向声明与测试
       登记同步。
- [ ] 06. 名实一致,无过度承诺、无模糊、无误导性强调。
- [ ] 07. 收敛/轨迹/时序类测试覆盖端点与过程,含逐拍不变量或相态验证。
- [ ] 08. 对称语义的两侧独立测试;边界与退化配置已覆盖,含符号翻转与未定义行为回归。
- [ ] 09. 状态/模式切换分静态与动态;参数设定后的执行、切换、过渡已验证。
- [ ] 10. 无重言式断言、无多余断言;数值换算参数非退化;回环测试已以独立路径打破自洽。
- [ ] 11. 容差与典型偏差匹配,无过宽、无装饰性断言。
- [ ] 12. 断言针对被测输出而非替身调用;一个测试聚焦单一行为(同性质多拍不变量除外)。
- [ ] 13. 替身不调并发钩子、无外部过程模型;简化项已在头注释记录。
- [ ] 14. 关键断言已经变异测试验证有效性。