임베디드 단위 테스트 (명세 기반)은(는) 무엇을 하나요?
사용자가 전달한 .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로 되돌아가게 하는 것을 드러냅니다). 아래 단계가 전체 프로세스입니다.
| Phase | Aniche 단계 | 수행 작업 |
|---|---|---|
| 0 | — | 환경 감지 → Path A (Zephyr), B (ESP-IDF) 또는 C (기타) |
| 1 | 1 이해 | 각 주어진 함수의 요구사항, 입력, 출력 파악 |
| 2 | 2 코드 탐색 | 페이크/목으로 격리할 종속성 매핑 |
| 3 | 3 파티션 | 번호가 매겨진 파티션 카탈로그 (가장 중요한 단계) |
| 4 | 4 경계 | 번호가 매겨진 경계값 카탈로그 (on/off 점) |
| 5 | 5 케이스 설계 | 파티션/경계값 결합, 조합 축소 |
| 6 | 6 자동화 | 경로의 프레임워크로 스위트 작성, 추적성 포함 |
| 7 | — | 도구 실행: 실행 + 커버리지 |
| 8 | 7 보강 | 갭 (커버리지 + 테스트 없는 카탈로그 항목) 반복 개선 |
Phase 0 — 환경 감지
주어진 파일이 포함된 프로젝트의 환경을 다음 순서로 식별하고 경로를 선택합니다. Robot Framework, HIL, 통합 pytest는 단위 테스트 프레임워크가 아닙니다 — 어떤 경로에서도 포함되지 않습니다.
-
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를 읽으세요. -
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를 읽으세요. -
기타 (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 카탈로그는 스위트 내 주석이 되며, 두 수준으로 적용됩니다:
- 함수별 헤더 — 해당 함수의 테스트 섹션 상단에 전체 카탈로그 (각 파티션/경계값을 ID와 짧은 설명과 함께)를 나열하는 블록.
- 테스트별 주석 — 각 테스트가 실행하는 항목을 참조, 예:
/* 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을 사용하지 마십시오. 대신 두 가지 테스트를 작성하십시오:
- 문서화된 동작, 이유와 함께 무시됨 — 문서가 약속한 내용의 어설트, 이유와 함께 건너뜀:
ztest_test_skip()(Path A) 또는TEST_IGNORE_MESSAGE("BUG: ...")(Path B). 위 주석:/* Covers: Pn — BUG: <문서는 X라고 말하지만; 코드는 Y를 수행함> */. - 특성화 — 현재 동작을 고정하여, 이후 수정 사항을 포함한 모든 변경이 드러나도록 합니다. 주석:
/* 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 이후, 또는 사용자가 요청할 때)
다음 내용을 포함하는 간결한 요약을 출력하십시오:
- 경로 및 환경: 감지된 경로와 이를 정당화하는 시그널.
- 테스트 스위트: 생성된 파일 경로.
- 커버리지 결과: 함수, 라인, 브랜치 백분율.
- 갭: 미커버리지 라인/브랜치 및 카탈로그 항목.
- 발견된 결함: 문서와 다른 동작.
- 아티팩트 위치: 생성된 파일 및 보고서 경로.
요약은 의사 결정을 위한 것입니다 — 사용자가 무엇을 검토하고, 변경하고, 저장해야 하는지 알 수 있어야 합니다.
참조 파일
references/specification-based-testing.md— 파티션/경계값 방법론.references/zephyr-path.md— Zephyr 관련 세부 사항.references/espidf-path.md— ESP-IDF 관련 세부 사항.references/non-zephyr-path.md— 기타 환경 관련 세부 사항.