Community라이팅 & 에디팅github.com

임베디드 단위 테스트 (명세 기반)

주어진 .c/.h 파일에 대해 UNIT TEST 스위트를 생성합니다. Maurício Aniche의 명세 기반 테스트 방법론(파티션 → 경계 → 케이스 → 커버리지)을 따릅니다. Zephyr, ESP-IDF 및 기타 환경을 자동 감지하여 각 환경에 적합한 테스트 프레임워크를 적용합니다.

임베디드 단위 테스트 (명세 기반)란 무엇인가요?

이 스킬은 임베디드 펌웨어의 .c/.h 파일에 대한 단위 테스트 스위트를 생성하는 전문 도구입니다. Maurício Aniche의 명세 기반 테스트(Specification-Based Testing) 방법론의 7단계를 실제 프로젝트에 적용하여, 함수의 입력 도메인을 효과적으로 분할(파티셔닝)하고 경계값을 도출하며 최소한의 테스트 케이스로 버그를 잡아내는 데 중점을 둡니다. 이 스킬이 지원하는 플랫폼은 Zephyr RTOS (twister + ztest + FFF + native_sim), Espressif ESP-IDF (네이티브 Unity + gcovr), 그리고 기타 임베디드 프로젝트(Unity, CMock, Ceedling, GoogleTest 등 기존 프레임워크 자동 감지)입니다. 환경을 자동으로 감지하여(Phase 0) 각 플랫폼에 최적화된 테스트 인프라를 구축합니다. 작동 방식은 9단계 프로세스로 이루어집니다: 환경 감지 → 요구사항 이해 → 종속성 분석 → 파티션 식별(가장 중요한 단계) → 경계 분석 → 테스트 케이스 설계 → 자동화(추적 가능한 주석 포함) → 실행 및 커버리지 측정 → 커버리지 갭 반복 개선. 최종 목표는 함수 95%, 라인 95%, 브랜치 90%의 커버리지를 달성하는 것입니다. 사용자가 요청할 경우 100% 목표도 지원합니다. 이 스킬은 자율적으로 작동하며, 사용자에게 확인을 요청하지 않고 테스트를 생성, 실행 및 보고합니다. 단, 두 가지 예외 상황(지원되지 않는 환경에서 테스트 프레임워크를 찾을 수 없거나, 커버리지 목표에 도달하지 못한 반복이 2회 이상 지속될 경우)에서만 사용자에게 질문합니다. 모든 출력은 영어로 생성되며, 통합 테스트, E2E, HIL, 시스템 테스트는 생성하지 않습니다.

지원 대상✓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에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

임베디드 단위 테스트 (명세 기반)은(는) 무엇을 하나요?

사용자가 전달한 .c/.h 파일 세트에 대해 단위 테스트 스위트를 생성하며, Maurício Aniche의 명세 기반 테스트 7단계 (Effective Software Testing, 2장)를 사용합니다. 이 스킬의 핵심 판단 능력은 각 함수의 도메인을 잘 분할하는 것(가장 가치 있는 단계)이며, 경계값을 도출하고 버그를 잡을 수 있는 최소한의 스위트를 설계한 후, 프로젝트에 적합한 프레임워크로 자동화하고 커버리지 목표에 도달할 때까지 반복합니다.

Phase 3 이전에 references/specification-based-testing.md를 읽으십시오. 이는 파티션과 경계값을 어떻게 생각해야 하는지, 그리고 스위트가 비대해지거나 취약해지는 것을 방지하는 실용적인 지침을 제공하는 교정 문서입니다.

범위 불변 규칙 (먼저 읽으세요)

  • 영어 출력. 이 스킬이 생성하는 모든 것은 영어입니다. 사용자가 다른 언어로 작성하더라도, 테스트 코드, 테스트 이름, 주석, 카탈로그, Covers: 주석, skip/ignore 메시지, 경로 선언 및 최종 요약 모두 영어입니다.
  • 단위 테스트만. 통합, E2E, 시스템, HIL 또는 Robot Framework 테스트는 절대 생성하지 않습니다. 세분화 수준은 격리된 함수/모듈이며, 종속성은 페이크/목으로 처리합니다.
  • 주어진 파일만. 스위트는 사용자가 전달한 .c/.h 세트에 대해서만 생성합니다. 프로젝트를 스캔하여 다른 대상을 찾거나 이웃 모듈을 임의로 테스트하지 마십시오.
  • 대상 파일을 절대 수정하지 마십시오 — 임시로라도, "테스트하고 되돌리기" 위해서라도 안 됩니다. 스위트는 원본 .c 파일을 컴파일하며, 테스트 디렉토리에 복사본을 만들지 않습니다. 접근하기 어려운 부분을 테스트하려면 공개 API, 종속성의 페이크 또는 별도의 테스트 바이너리를 사용하세요.
  • 자율적으로 동작. 실행을 확인받지 않고 생성, 실행 및 보고합니다. 두 가지 예외 상황에서만 멈추고 질문합니다: (a) Zephyr나 ESP-IDF가 아닌 Path C에서 단위 테스트 프레임워크를 식별할 수 없는 경우 (Phase 0); (b) 커버리지 목표에 도달하지 못한 반복이 2회 이상 지속된 경우 (Phase 8).
  • 당신이 보고서를 작성하지 않음. 결과 보고서를 작성하지 않습니다. 도구를 실행하고 도구가 생성한 아티팩트 (커버리지 HTML, 러너 출력)를 가리킵니다. "결과"는 도구가 출력하는 것이지, 당신의 산문이 아닙니다.
  • 커버리지 목표: 기본값으로 함수 ≥ 95%, 라인 ≥ 95%, 브랜치 ≥ 90% (95/95/90). 목표에 도달하면 반복을 멈추고 남아있는 미커버리지 라인/브랜치를 나열합니다. 사용자가 요청할 경우에만 100/100/100을 목표로 합니다. 목표 미만은 증명 가능하게 도달할 수 없는 부분(로그/어설트 노이즈, 모듈의 테스트 가능성 제한)에 대해서만 허용되며, 각 남은 라인/브랜치에 대해 이유를 설명해야 합니다.

흐름 (Aniche의 7단계 → 9단계)

7단계는 엄격한 파이프라인이 아닙니다 — 반복적입니다 (Phase 3은 종종 Phase 1로 되돌아가게 하는 것을 드러냅니다). 아래 단계가 전체 프로세스입니다.

PhaseAniche 단계수행 작업
0—환경 감지 → Path A (Zephyr), B (ESP-IDF) 또는 C (기타)
11 이해각 주어진 함수의 요구사항, 입력, 출력 파악
22 코드 탐색페이크/목으로 격리할 종속성 매핑
33 파티션번호가 매겨진 파티션 카탈로그 (가장 중요한 단계)
44 경계번호가 매겨진 경계값 카탈로그 (on/off 점)
55 케이스 설계파티션/경계값 결합, 조합 축소
66 자동화경로의 프레임워크로 스위트 작성, 추적성 포함
7—도구 실행: 실행 + 커버리지
87 보강갭 (커버리지 + 테스트 없는 카탈로그 항목) 반복 개선

Phase 0 — 환경 감지

주어진 파일이 포함된 프로젝트의 환경을 다음 순서로 식별하고 경로를 선택합니다. Robot Framework, HIL, 통합 pytest는 단위 테스트 프레임워크가 아닙니다 — 어떤 경로에서도 포함되지 않습니다.

  1. Zephyr? → Path A. 시그널: west.yml/west.yaml; find_package(Zephyr ...)가 있는 CMakeLists.txt; prj.conf; testcase.yaml/sample.yaml; tests/; ztest/ZTEST_SUITE. 도구: twister + ztest + FFF + 커버리지. references/zephyr-path.md를 읽으세요.

  2. ESP-IDF? → Path B. 시그널: idf_component_register(가 있는 CMakeLists.txt; 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도 아님) → Path C. 기존 단위 테스트 구조 (Unity, CMock, Ceedling, GoogleTest, 사용자 정의)를 검사하고 프로젝트의 패턴을 충실히 따르세요. 단위 테스트 프레임워크를 찾을 수 없으면, 멈추고 사용자에게 어떤 것을 사용할지 물어보세요 — Path C에서 조용히 기본값을 가정하지 마십시오. references/non-zephyr-path.md를 읽으세요.

응답 텍스트에 상태를 명시하십시오 (추론뿐만 아니라), 다음 단계로 넘어가기 전에 선택한 경로와 이를 정당화하는 구체적인 시그널을 파일과 발견한 내용과 함께 예를 들어 "Path A (Zephyr): zephyr/west.yml, tests/.../CMakeLists.txt의 find_package(Zephyr), tests/lib/의 ZTEST_SUITE 스위트"와 같이 작성하십시오.


Phase 1 — 요구사항, 입력 및 출력 이해

스위트가 다룰 각 함수에 대해 .c/.h를 읽어 다음을 추출합니다:

  • 비즈니스 규칙: 무엇을 하고 무엇을 하지 말아야 하는지 (코너 케이스 포함);
  • 입력: 타입, 범위/도메인, 변수 간 제약 조건;
  • 출력: 입력이 결과가 되는 방식 (및 관찰 가능한 부작용: 출력 기록, 오류 반환, 모션 방출/미방출).

이 중 일부는 명시적이고, 일부는 암묵적인 도메인 규칙 (포화, 랩, 단위, 부호, 백래시)으로 추론해야 합니다. 이러한 암묵적 규칙이 버그가 숨는 곳입니다.

Phase 2 — 코드 탐색

전체 .c/.h를 읽고 격리할 종속성 맵을 구축합니다: HAL, 드라이버, 커널/RTOS, I/O, 시간, 다른 모듈 호출. 각 종속성은 Phase 6에서 페이크/목이 됩니다. 또한 실제 로직에 도달하는 방법 (직접적인 공개 API, 또는 핸들/vtable을 통한 static 함수 — 실제 API를 통해 호출하고, 시그니처를 절대 임의로 만들지 마십시오)을 식별합니다.

Phase 3 — 파티션 식별 (가장 중요한 단계)

references/specification-based-testing.md의 3가지 렌즈를 각 함수에 적용합니다: 각 입력을 개별적으로, 입력을 조합하여, 그리고 출력 클래스. NULL/비어 있음 및 암묵적 도메인 규칙을 고려합니다. 함수에 상태가 있다면 모듈/컨텍스트의 상태로도 파티션합니다.

함수별로 스위트 내에서 안정적인 ID를 가진 이름이 지정되고 번호가 매겨진 카탈로그를 생성합니다:

  • 파티션: <func>/P1, P2, …
  • 경계값 (Phase 4에서 채움): <func>/B1, B2, …

각 항목은 짧은 설명을 가집니다. 이 카탈로그는 스위트의 추적성 계약 (Phase 6에서 구체화됨)이며 갭 (Phase 8)의 진실 공급원입니다. 잘 수행된 Phase 3은 첫 번째 커버리지 패스에서 거의 모든 것을 다루어야 합니다.

Phase 4 — 경계 분석

인접한 파티션 간의 각 경계에 대해 카탈로그에 온 포인트 (조건의 값)와 오프 포인트 (반대 파티션에서 가장 가까운 이웃)를 추가합니다; 동등 조건은 하나의 온과 두 개의 오프 포인트를 제공합니다. 이미 다른 케이스에서 다루어진 경계는 제거하십시오 — 중복하지 마십시오. "파티션 또는 경계? 상관없음": 중요한 것은 케이스가 존재한다는 것입니다.

Phase 5 — 테스트 케이스 설계

입력 범주 간의 파티션을 결합하되 조합 폭발을 줄이십시오: 예외적인 것 (NULL/비어 있음)은 한 번만 테스트하고 모든 것과 결합하지 마십시오; 도메인을 사용하여 잘라내십시오. 단일 시드 입력을 사용하고, 케이스당 최소한의 변동만 적용하십시오 (디버깅 용이). 의심스러울 때는 가장 간단한 입력을 사용하십시오. 동작에 참여하지 않는 입력은 합리적/현실적인 값을 사용하십시오. 케이스가 여러 다른 케이스와 동일한 골격을 가지고 있다면 매개변수화된 테스트로 계획하십시오. 케이스를 나열하고, 각각이 실행하는 카탈로그 ID를 명시하십시오.

Phase 6 — 자동화 (필수 추적성 포함)

감지된 경로의 프레임워크로 스위트를 작성하십시오 (구문, 격리 및 스캐폴드 세부 사항은 references/zephyr-path.md, references/espidf-path.md 또는 references/non-zephyr-path.md 참조).

Phase 3/4 카탈로그는 스위트 내 주석이 되며, 두 수준으로 적용됩니다:

  1. 함수별 헤더 — 해당 함수의 테스트 섹션 상단에 전체 카탈로그 (각 파티션/경계값을 ID와 짧은 설명과 함께)를 나열하는 블록.
  2. 테스트별 주석 — 각 테스트가 실행하는 항목을 참조, 예: /* Covers: P3 + B1 */.

필수 규칙 (이것이 추적성을 가치 있게 만듭니다):

  • 고아 테스트 없음: 모든 테스트는 ≥ 1개의 카탈로그 항목을 참조합니다.
  • 고아 항목 없음: 모든 파티션/경계값은 ≥ 1개의 테스트에 의해 실행됩니다. 테스트가 없는 항목은 갭으로 플래그 지정됩니다 (Phase 8에 입력).
  • 안정적이고 동일한 ID가 헤더와 주석 사이에 유지되어야 — grep "P4"가 헤더와 해당 파티션을 다루는 모든 테스트를 나열할 수 있어야 합니다.
  • 조합을 다루는 테스트는 모든 ID를 나열합니다 (/* Covers: P2 + P5 */).
  • Covers:는 진실을 말합니다: 각 주석 처리된 ID는 해당 케이스가 실제로 실행하는 것 (입력, 상태 및 어설트)과 일치해야 하며, 카탈로그에 존재하는 항목만이 아닙니다. Phase 7 전에 각 테스트 본문에 대해 주석을 다시 읽고 일치하지 않는 것을 수정하십시오.
  • Path B 및 C에서는 동일한 체계를 프레임워크 구문에 맞게 조정하십시오 (각 TEST(...)/TEST_CASE(...)/TEST_F(...) 위에 주석).

정규 형식 (Path A, ztest):

/*
 * 카탈로그 — motor_ctrl_transform  (Phase 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() (Path A) 또는 TEST_IGNORE_MESSAGE("BUG: ...") (Path B). 위 주석: /* Covers: Pn — BUG: <문서는 X라고 말하지만; 코드는 Y를 수행함> */.
  2. 특성화 — 현재 동작을 고정하여, 이후 수정 사항을 포함한 모든 변경이 드러나도록 합니다. 주석: /* Covers: Pn — 위 BUG의 특성화 */.

스위트는 초록색으로 끝나고, 버그는 매 실행마다 (skip/ignore로) 나타나며, 이진 파일은 측정에 포함됩니다. 구문은 경로 파일에 있습니다. 최종 요약에 결함을 나열하십시오.

Phase 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). 요약이 실패를 설명하지 못할 때만 전체 로그를 여십시오.
  • 각 테스트 파일을 한 번에 전체 작성하고, 타겟 편집으로 수정하십시오.
  • 독립적인 읽기 및 검사를 동일한 턴에 그룹화하고, 이미 읽은 파일을 다시 읽지 마십시오.

경로의 도구를 실행하고 생성된 아티팩트를 가리키십시오 (보고서를 작성하지 마십시오):

  • Path 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.
  • Path B (ESP-IDF): Unity + gcov/gcovr로 호스트 빌드 (espidf-path.md 참조); 커버리지 보고서 (coverage.html/.xml) 및 테스트 출력을 가리킵니다.
  • Path C (기타): 프로젝트 자체 러너 + 커버리지 (예: ceedling gcov, 또는 테스트 빌드에 대한 gcov/gcovr). 생성된 아티팩트를 유지하고 가리킵니다.

각 경로 파일의 세부 사항 및 문제 해결.

Phase 8 — 커버리지 반복

도구에서 보고된 커버리지를 95/95/90 목표 (또는 사용자가 요청한 목표)와 비교하십시오. 목표 미만일 경우, 테스트가 없는 모든 도달 가능한 라인/브랜치는 갭입니다; 목표에 도달하면 멈추고 미커버리지 상태를 나열하십시오. 각 갭에 대해 두 가지 소스를 모두 사용하여 케이스를 추가하십시오: 숫자 커버리지 (빨간 라인/브랜치) 및 카탈로그 (테스트가 없는 항목).

추가된 케이스로 Phase 7로 돌아가십시오. 목표에 도달할 때까지 또는 두 번의 연속 반복에서 목표에 도달하지 못할 때까지 반복하십시오. 두 번의 반복 후에는 멈추고 사용자에게 물어보십시오: "지금까지 추가된 케이스가 커버리지에 영향을 미치지 않았습니다. 진행 방법을 알려주십시오."

목표에 도달하면 (100/100/100이 아닌 95/95/90에서도) 나머지 미커버리지 라인/브랜치를 나열하고, 이유를 설명하십시오. 그런 다음 완료하십시오.


최종 요약 (Phase 8 이후, 또는 사용자가 요청할 때)

다음 내용을 포함하는 간결한 요약을 출력하십시오:

  1. 경로 및 환경: 감지된 경로와 이를 정당화하는 시그널.
  2. 테스트 스위트: 생성된 파일 경로.
  3. 커버리지 결과: 함수, 라인, 브랜치 백분율.
  4. 갭: 미커버리지 라인/브랜치 및 카탈로그 항목.
  5. 발견된 결함: 문서와 다른 동작.
  6. 아티팩트 위치: 생성된 파일 및 보고서 경로.

요약은 의사 결정을 위한 것입니다 — 사용자가 무엇을 검토하고, 변경하고, 저장해야 하는지 알 수 있어야 합니다.


참조 파일

  • references/specification-based-testing.md — 파티션/경계값 방법론.
  • references/zephyr-path.md — Zephyr 관련 세부 사항.
  • references/espidf-path.md — ESP-IDF 관련 세부 사항.
  • references/non-zephyr-path.md — 기타 환경 관련 세부 사항.

관련 스킬