Community写作与编辑github.com

嵌入式单元测试(基于规格说明)

为嵌入式固件的 .c/.h 文件生成单元测试套件,遵循 Maurício Aniche 的基于规格说明的测试方法(分区 → 边界 → 用例 → 覆盖率)。自动检测环境:Zephyr 使用 twister + ztest + FFF 在 native_sim 上运行;ESP-IDF 使用原生 Unity 主机构建 + gcovr;其他环境则遵循项目已有的测试框架,或停止并询问用户。

嵌入式单元测试(基于规格说明) 是什么?

该技能为嵌入式固件模块(.c/.h 文件)自动生成单元测试套件,基于 Maurício Aniche 在《Effective Software Testing》中提出的七步规格说明测试方法。其核心价值在于:正确划分每个函数的输入域(最关键步骤)、推导边界值、设计最小但能有效捕获缺陷的测试用例,并在项目的正确测试框架中自动化执行,迭代直至达到所需覆盖率。该技能支持 Zephyr(twister + ztest + FFF)、ESP-IDF(原生 Unity + gcovr)以及其他项目已有框架(Unity、CMock、Ceedling、GoogleTest 等)。工作流程分为9个阶段,从环境检测、理解需求、探索代码、划分分区、分析边界、设计用例、自动化编写(含强制可追溯性注释)、执行与覆盖率收集,到基于覆盖率缺口迭代补充测试。它坚持“不修改目标文件”、“不扫描项目其他目标”、“不生成集成/端到端测试”等严格不变规则,并默认以函数≥95%、行≥95%、分支≥90%作为覆盖率目标。

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/PedroGilo12/claude-skills/tree/HEAD/plugins/firmware-testing/skills/embedded-unit-testing

Installed? Explore more 写作与编辑 skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

嵌入式单元测试(基于规格说明) 是做什么的?

为用户提供的 .c/.h 文件生成单元测试套件,遵循 Maurício Aniche 的基于规格说明测试的 7 个步骤(《有效的软件测试》第 2 章)。作为技能的核心判断是:正确划分每个函数的输入域(最有价值的步骤),推导边界值,设计最小但能捕获缺陷的测试用例,在项目的正确测试框架中自动化执行,并迭代直至达到所需覆盖率。

在第 3 阶段之前阅读 references/specification-based-testing.md——它为您提供了关于如何思考分区和边界的校准,以及避免测试套件臃肿或脆弱的具体实践。

范围不变规则(请先阅读)

  • 英文输出。 此技能产生的所有内容均为英文,即使使用其他语言编写:测试代码、测试名称、注释、目录、Covers: 注解、跳过/忽略消息、路径声明以及最终摘要。
  • 仅限单元测试。 绝不生成集成、端到端、系统、HIL 或 Robot Framework 测试。粒度为隔离的函数/模块,依赖项使用假对象/模拟对象。
  • 仅处理给定的文件。 测试套件针对用户传递的 .c/.h 文件集合。不要扫描项目以查找其他目标,也不要在未指示的情况下自行测试相邻模块。
  • 切勿修改目标文件——无论是临时修改还是“测试后恢复”。测试套件编译原始的 .c 文件,不复制到测试目录。为访问难以测试的部分,使用公共 API、依赖项的假对象或单独的测试二进制文件。
  • 自主运行。 生成、运行并报告,无需请求确认执行。有两个例外情况您需要停止并询问:(a) 路径 C(既不是 Zephyr 也不是 ESP-IDF)且无法识别任何单元测试框架(第 0 阶段);(b) 连续 2 次覆盖率迭代均未达到目标(第 8 阶段)。
  • 您不撰写报告。 您不编写结果报告。运行工具并指向它们生成的产物(覆盖率 HTML、运行器输出)。“结果”是工具发出的内容,而不是您的叙述。
  • 覆盖率目标: 默认函数≥95%,行≥95%,分支≥90%(95/95/90)。一旦达到目标,停止迭代并列出剩余未覆盖的行/分支。仅当用户要求时才追求 100/100/100。低于目标仅当缺失部分被证明是不可达的(日志/断言噪音、模块的可测试性限制),且需要为每个剩余的行/分支提供理由。

流程(Aniche 的 7 个步骤 → 9 个阶段)

这 7 个步骤不是严格的流水线——它们是迭代的(第 3 阶段通常会揭示某些内容,使您回到第 1 阶段)。以下阶段是主干。

阶段Aniche 步骤您要做的事
0—检测环境 → 路径 A(Zephyr)、B(ESP-IDF)或 C(其他)
11 理解每个给定函数的需求、输入、输出
22 探索代码绘制依赖关系图以使用假对象/模拟对象隔离
33 分区编号的分区目录(最重要的一步)
44 边界编号的边界目录(开/关点)
55 设计用例组合分区/边界,减少组合数量
66 自动化使用路径的框架编写测试套件,包含可追溯性
7—运行工具:执行 + 覆盖率
87 扩充迭代缺口(覆盖率 + 目录中无测试的项)

第 0 阶段 — 环境检测

按此顺序识别包含给定文件的项目环境,并选择路径。Robot Framework、HIL、集成 pytest 不是单元测试框架——它们在任何路径中都不算数。

  1. Zephyr?→ 路径 A。 信号:west.yml/west.yaml;CMakeLists.txt 包含 find_package(Zephyr ...);prj.conf;testcase.yaml/sample.yaml;tests/;ztest/ZTEST_SUITE。工具:twister + ztest + FFF + 覆盖率。阅读 references/zephyr-path.md。

  2. ESP-IDF?→ 路径 B。 信号:CMakeLists.txt 包含 idf_component_register(;sdkconfig/sdkconfig.defaults;使用 $ENV{IDF_PATH}/idf.py;main/ 组件。工具:ESP-IDF 的原生 Unity(主机构建,linux 目标)+ gcov/gcovr。直接使用 Unity——不要停止询问。例外:如果项目已经始终使用另一个单元测试框架(Ceedling、GoogleTest…),则遵循该框架。阅读 references/espidf-path.md。

  3. 其他(既不是 Zephyr 也不是 ESP-IDF)→ 路径 C。 检查现有的单元测试结构(Unity、CMock、Ceedling、GoogleTest、自定义)并忠实地遵循项目的模式。如果未找到单元测试框架,则停止并询问用户使用哪个——切勿在路径 C 中假设默认值。阅读 references/non-zephyr-path.md。

在响应文本中说明(不仅在您的推理中),在继续之前,说明所选路径以及证明其合理性的具体信号,包括文件和您找到的内容——例如“路径 A(Zephyr):zephyr/west.yml,tests/.../CMakeLists.txt 中的 find_package(Zephyr),tests/lib/ 中的 ZTEST_SUITE 套件。”


第 1 阶段 — 理解需求、输入和输出

对于测试套件将覆盖的每个函数,从阅读 .c/.h 中提取:

  • 业务规则:它做什么以及它不能做什么(包括边界情况);
  • 输入:类型、范围/域、变量之间的约束;
  • 输出:输入如何变为结果(以及可观察的副作用:输出是否写入、是否返回错误、是否发出/未发出运动)。

部分规则是显式的;部分是通过领域规则(饱和、回绕、单位、符号、齿隙)推断的,这些规则是隐含的。这些隐含规则正是 bug 隐藏的地方。

第 2 阶段 — 探索代码

阅读整个 .c/.h 并构建要隔离的依赖关系图:HAL、驱动程序、内核/RTOS、I/O、时间、对其他模块的调用。每个依赖项将成为第 6 阶段的假对象/模拟对象。还要确定如何访问实际逻辑(直接公共 API,或通过句柄/vtable 调用 static 函数——通过真实 API 调用它们,切勿发明签名)。

第 3 阶段 — 识别分区(最重要的一步)

将 references/specification-based-testing.md 中的 3 个视角应用于每个函数:单独考虑每个输入、输入组合以及输出类别。考虑 NULL/空和隐含的领域规则。如果函数有状态,还要根据模块/上下文的状态进行划分。

针对每个函数,生成一个有名称、有编号的目录,在测试套件中具有稳定的 ID:

  • 分区:<func>/P1, P2, …
  • 边界(在第 4 阶段填写):<func>/B1, B2, …

每个条目都有简短的描述。此目录是测试套件的可追溯性契约(在第 6 阶段实现)以及缺口(第 8 阶段)的真实来源。执行良好的第 3 阶段应在首次覆盖率通过时涵盖几乎所有内容。

第 4 阶段 — 分析边界

对于相邻分区之间的每个边界,在目录中添加开点(条件中的值)和关点(相反分区中最近的相邻值);相等条件给出一个开点和两个关点。删除已被其他用例覆盖的边界——不要重复。“分区还是边界?不重要”:关键是存在该用例。

第 5 阶段 — 设计测试用例

跨输入类别组合分区,但减少组合爆炸:测试异常情况(NULL/空)一次,不要将其与所有内容组合;使用领域知识来削减。使用单个种子输入,每个用例仅改变最小值(便于调试)。如有疑问,使用最简单的输入。对于不参与行为的输入,使用合理/实际的值。如果一个用例与多个其他用例具有相同的骨架,则将其规划为参数化。列出用例,并针对每个用例列出其涵盖的目录 ID。

第 6 阶段 — 自动化(带强制性可追溯性)

使用检测到的路径的框架编写测试套件(语法、隔离和模板的详细信息见 references/zephyr-path.md、references/espidf-path.md 或 references/non-zephyr-path.md)。

第 3/4 阶段的目录成为测试套件中的注释,分为两个级别:

  1. 每个函数的头部——在该函数测试部分顶部的块,列出完整目录(每个分区/边界及其 ID 和简短描述)。
  2. 每个测试的注解——每个测试引用其涵盖的项,例如 /* Covers: P3 + B1 */。

强制性规则(这是使可追溯性有价值的关键):

  • 无孤立测试: 每个测试引用 ≥ 1 个目录项。
  • 无孤立项: 每个分区/边界由 ≥ 1 个测试覆盖。没有测试的项被标记为缺口(供第 8 阶段使用)。
  • 头部和注解之间 ID 稳定且相同——以便 grep "P4" 列出头部和触及该分区的每个测试。
  • 覆盖组合的测试需列出所有 ID(/* Covers: P2 + P5 */)。
  • Covers: 必须真实: 每个注解的 ID 必须与用例实际覆盖的内容(输入、状态和断言)匹配,而不仅仅是目录中存在的项。在第 7 阶段之前,根据每个测试主体重新阅读注解,并修复不匹配的注解。
  • 在路径 B 和 C 中,采用相同的方案,但需适应框架的语法(在每个 TEST(...)/TEST_CASE(...)/TEST_F(...) 上方添加注释)。

规范形式(路径 A,ztest):

/*
 * Catalog — motor_ctrl_transform  (Phase 3 · spec-based testing)
 * Partitions:
 *   P1  p_axis_values == NULL        -> rejected, output not written
 *   P2  valid axis_count (1..N)      -> transforms
 *   P3  positive value               -> positive steps
 *   P4  negative value               -> sign preserved
 *   P5  zero value                   -> zero steps
 * Boundaries:
 *   B1  axis_count == N (max)        -> accepted
 *   B2  axis_count == N+1            -> rejected
 */

/* Covers: P2 + P3 */
ZTEST(motor_ctrl_suite, test_transform_positive_gives_positive_steps) { /* ... */ }

/* Covers: P1 */
ZTEST(motor_ctrl_suite, test_transform_null_input_is_rejected) { /* ... */ }

注意:在较新的 Zephyr 中,运行器要求测试名称以 test_ 开头(ZTEST(suite, test_...))——没有此前缀,twister 不会收集该用例。

发现的缺陷:代码与文档不符

当代码与头文件、函数注释或 API 合同文档相矛盾时(例如,返回与文档不同的错误代码,失败时未恢复状态),不要使测试套件变红,也不要使用 ZTEST_EXPECT_FAIL。编写两个测试:

  1. 文档行为,附原因并忽略——对文档承诺的断言,但被跳过并附带原因:ztest_test_skip()(路径 A)或 TEST_IGNORE_MESSAGE("BUG: ...")(路径 B)。上方注释:/* Covers: Pn — BUG: <docs say X; code does Y> */。
  2. 特征描述——固定当前行为,以便任何更改(包括修复)都能显现。注释:/* Covers: Pn — characterization of the BUG above */。

测试套件以绿色结束,bug 在每次运行中都能显现(跳过/忽略),并且二进制文件进入测量。在最终摘要中列出缺陷。

第 7 阶段 — 执行 + 覆盖率

在运行之前,激活项目环境。 测试工具很少在全局 PATH 上——它通常位于仓库的虚拟环境中。在得出结论“无法运行”之前,请先查找并激活环境(这适用于所有路径):

  • 在固件根目录下查找 venv(.venv/、venv/、env/),并使用其二进制文件(.venv/bin/west、.venv/bin/python)或 source .venv/bin/activate。该 venv 通常是符号链接:使用 test -e .venv/bin/activate 或 ls -laL(跟随链接)检查,切勿仅使用 ls -d/find -type d。说明您找到了什么:所使用的 venv 路径,或“在 <检查的目录> 中未找到 venv”。
  • Zephyr: 设置 ZEPHYR_BASE(例如 deps/zephyr,或 west topdir/.west/config),对于主机,设置 ZEPHYR_TOOLCHAIN_VARIANT=host。如果 west 不在 PATH 上但 Zephyr 树存在,则直接运行 twister:python3 $ZEPHYR_BASE/scripts/twister ...。
  • ESP-IDF: 确保 IDF 环境已设置(存在时执行 . $IDF_PATH/export.sh)以进行主机构建;否则直接使用 gcc 编译(参见 espidf-path.md)。

仅在穷举此搜索后,才可诚实地报告无法在此处运行。

上下文经济(固件构建冗长,每次重新读取所有内容):

  • 将构建、twister 和 gcovr 输出重定向到文件(> build.log 2>&1),仅显示摘要(tail -n 30 build.log、grep -E 'error|FAIL|PASS|lines|branches' build.log)。仅当摘要无法解释失败时才打开整个日志。
  • 一次性完整写入每个测试文件;通过有针对性的编辑进行修复。
  • 在同一轮中组合独立的读取和检查;不要重新读取您已经读取过的文件。

运行路径的工具并指向其生成的产物(不要编写报告):

  • 路径 A(Zephyr): west twister --coverage -p native_sim -T <suite_path>。产物:twister-out/coverage/index.html、twister-out/coverage.json、twister-out/twister.xml、twister-out/twister.json。
  • 路径 B(ESP-IDF): 使用 Unity + gcov/gcovr 进行主机构建(参见 espidf-path.md);指向覆盖率报告(coverage.html/.xml)和测试输出。
  • 路径 C(其他): 项目自己的运行器 + 覆盖率(例如 ceedling gcov,或通过测试构建的 gcov/gcovr)。保留并指向发出的产物。

每个路径文件中的详细信息和故障排除。

第 8 阶段 — 覆盖率迭代

将工具报告的覆盖率与 95/95/90 目标(或用户要求的目标)进行比较。低于目标时,每个没有测试的可达行/分支都是一个缺口;一旦达到目标,停止并列出未覆盖的内容。对于每个缺口,使用两个来源添加用例:数值覆盖率(红色行/分支)和目录中尚未有测试的项。

相关技能