Community寫作與編輯github.com

嵌入式單元測試(基於規格)

為嵌入式韌體的 .c/.h 檔案生成單元測試套件,遵循 Maurício Aniche 的規格化測試方法(分割 → 邊界 → 測試案例 → 覆蓋率),並自動偵測 Zephyr、ESP-IDF 或其他專案的測試框架。

嵌入式單元測試(基於規格) 是什麼?

這是一個專門為嵌入式韌體開發的 AI 技能,能夠基於使用者提供的 .c/.h 檔案自動生成完整的單元測試套件。它遵循 Maurício Aniche 在《Effective Software Testing》第 2 章提出的規格化測試七步驟,核心在於對每個函式的輸入域進行有效的分割(Partitioning),這是發現 Bug 最有價值的步驟。該技能會自動偵測專案環境:Zephyr 使用 twister + ztest + FFF 搭配 native_sim 進行測試;ESP-IDF 使用原生的 Unity 測試框架並在宿主機編譯後透過 gcovr 分析覆蓋率;其他專案則遵循既有的測試框架(Unity、CMock、Ceedling、GoogleTest 等),若無法識別任何框架則會停下詢問使用者。它強制要求覆蓋率目標為函式 ≥ 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 步驟(Effective Software Testing,第 2 章)。此技能之所以成為技能的關鍵判斷在於:良好地分割每個函式的域(最有價值的步驟)、推導出邊界、設計出仍能捕捉錯誤的最小套件、在專案的正確框架中自動化測試,並迭代直到覆蓋率達到目標。

在第三階段之前請閱讀 references/specification-based-testing.md——這是關於如何思考分割與邊界的校準文件,以及避免套件膨脹或脆弱的實用方針。

範圍不變量(請先閱讀)

  • 英文輸出。 此技能產出的所有內容均為英文,即使使用者以其他語言撰寫:測試程式碼、測試名稱、註解、目錄、Covers: 註解、跳過/忽略訊息、路徑宣告以及最終摘要。
  • 僅限單元測試。 絕不生成整合測試、端到端測試、系統測試、HIL 測試或 Robot Framework 測試。顆粒度為孤立函式/模組,依賴關係使用 fake/mock。
  • 僅限給定的檔案。 套件僅針對使用者傳入的 .c/.h 檔案集合。不要掃描專案尋找其他目標,也不要自行測試相鄰模組。
  • 永遠不要修改目標檔案——即使是暫時的或「為了測試再還原」也不行。套件編譯原始的 .c 檔案,測試目錄中不複製。若要測試難以觸及的內容,請使用公開 API、依賴項的 fake 或獨立的測試二進位檔案。
  • 自主執行。 生成、執行並報告,無需請求確認即可執行。有兩個例外情況需要停下並詢問:(a) 路徑 C(非 Zephyr 也非 ESP-IDF)且無法識別任何單元測試框架(第 0 階段);(b) 每兩次覆蓋率迭代仍未達到目標(第 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 探索程式碼建立需用 fake/mock 隔離的依賴關係圖
33 分割編號的分割目錄(最重要的步驟)
44 邊界編號的邊界目錄(on point 與 off point)
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 階段都會變成一個 fake/mock。同時確定實際邏輯如何被觸及(直接公開 API,或經由 handle/vtable 的 static 函式——透過真實 API 呼叫它們,永遠不要自己發明簽名)。

第 3 階段——識別分割(最重要的步驟)

對每個函式應用 references/specification-based-testing.md 中的三個透鏡:分別考慮每個輸入、輸入的結合以及輸出類別。考慮 NULL/空值以及隱含的域規則。如果函式有狀態,則也根據模組/上下文的狀態進行分割。

針對每個函式,輸出一個命名的、編號的目錄,在套件內具有穩定的 ID:

  • 分割:<func>/P1, P2, …
  • 邊界(在第 4 階段填入):<func>/B1, B2, …

每個項目都有一個簡短描述。此目錄是套件的可追溯性合約(在第 6 階段實現),也是差距的真相來源(第 8 階段)。良好的第 3 階段應在第一次覆蓋率通過時涵蓋幾乎所有內容。

第 4 階段——分析邊界

對於相鄰分割之間的每個邊界,將on point(條件中的值)和off point(對立分割中最接近的鄰值)加入目錄;等於條件給出一個 on 點和兩個 off 點。如果邊界已被其他案例涵蓋則刪除——不要重複。「分割或邊界?不重要」:重要的是案例存在。

第 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):

/*
 * 目錄 — motor_ctrl_transform  (第 3 階段 · 基於規格的測試)
 * 分割:
 *   P1  p_axis_values == NULL        -> 拒絕,不寫入輸出
 *   P2  有效的 axis_count (1..N)      -> 變換
 *   P3  正值               -> 正步數
 *   P4  負值               -> 保留符號
 *   P5  零值                   -> 零步數
 * 邊界:
 *   B1  axis_count == N (最大值)        -> 接受
 *   B2  axis_count == N+1            -> 拒絕
 */

/* 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:<文件說 X;程式碼做 Y> */。
  2. 特徵描述——固定當前行為,這樣任何變更(包括修復)都會顯現。註解:/* Covers: Pn — 上述 BUG 的特徵描述 */。

套件以綠色結束,Bug 在每次執行中都顯現(作為跳過/忽略),且二進位檔案進入測量範圍。在最終摘要中列出缺陷。

第 7 階段——執行 + 覆蓋率

在執行前,啟動專案環境。 測試工具很少在全局 PATH 上——它通常存在於儲存庫的虛擬環境中。在得出「無法執行」的結論之前,請先尋找並啟動環境(這適用於每個路徑):

  • 在韌體根目錄尋找虛擬環境(.venv/、venv/、env/)並使用其二進位檔案(.venv/bin/west、.venv/bin/python)或 source .venv/bin/activate。虛擬環境通常是符號連結:使用 test -e .venv/bin/activate 或 ls -laL(追蹤連結)檢查,永遠不要只使用 ls -d/find -type d。說明你發現了什麼:所使用的虛擬環境路徑,或「在 <檢查的目錄> 中沒有虛擬環境」。
  • 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 目標(或使用者要求的目標)。低於目標時,每個可到達但沒有測試的行/分支都是一個差距;一旦達到目標,停止並列出未被覆蓋的內容。對於每個差距,使用兩個來源添加案例:數值覆蓋率(紅色的行/分支)以及……

[為翻譯而截斷]

相關技能