Use when implementing any feature or bugfix in AIM, before writing implementation code
Write the test first. Watch it fail. Write minimal code to pass.
Core principle: If you didn't watch the test fail, you don't know if it tests the right thing.
Violating the letter of the rules is violating the spirit of the rules.
Always:
Exceptions (ask user):
Thinking "skip TDD just this once"? Stop. That's rationalization.
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
Write code before the test? Delete it. Start over.
No exceptions:
Implement fresh from tests. Period.
digraph tdd_cycle {
rankdir=LR;
red [label="RED\nWrite failing gtest", shape=box, style=filled, fillcolor="#ffcccc"];
verify_red [label="dx make gtest\nfails correctly?", shape=diamond];
green [label="GREEN\nMinimal C code", shape=box, style=filled, fillcolor="#ccffcc"];
verify_green [label="dx make gtest\nall pass?", shape=diamond];
refactor [label="REFACTOR\nClean + dx make", shape=box, style=filled, fillcolor="#ccccff"];
next [label="Next", shape=ellipse];
red -> verify_red;
verify_red -> green [label="yes"];
verify_red -> red [label="wrong\nfailure"];
green -> verify_green;
verify_green -> refactor [label="yes"];
verify_green -> green [label="no"];
refactor -> verify_green [label="stay\ngreen"];
verify_green -> next;
next -> red;
}
Write one minimal GoogleTest showing what should happen.
EXPECT_EQ(rc, AIM_OK);
EXPECT_EQ(attempts, 3);
}
Clear name, tests real behavior, one thing
</Good>
<Bad>
```cpp
TEST(AimRetry, TestRetry) {
// just calls the function and checks return
EXPECT_EQ(aim_retry_operation(NULL, 0), 0);
}
Vague name, doesn't verify actual retry count </Bad>
Requirements:
MANDATORY. Never skip.
빌드 스코프 최소화 (시간 절약): RED 단계에서는 방금 추가한 테스트 바이너리만 빌드/실행한다. 전체 dx make gtest는 금지 — 불필요한 전체 재컴파일로 빌드 4회가 누적되어 태스크당 20분 이상 낭비된다.
# gtest 실행 전 서버 종료 (Text file busy 방지)
dx tmdown -y
# 방금 추가한 테스트 바이너리만 빌드/실행
dx bash -c "cd /root/ofsrc/aim/test/unit/gtest/src/<zone>/<module> && make -f Makefile_<target> && ./gtest_<module>_<target>"
Confirm:
Test passes? You're testing existing behavior. Fix test.
Build error? Fix build error first, re-run until it fails correctly.
Write simplest C code to pass the test.
Don't add features, refactor other code, or "improve" beyond the test.
MANDATORY.
빌드 스코프 최소화: 태스크 중간에는 수정한 모듈의 테스트만 실행한다. 전체 dx make gtest(전 모듈 회귀)는 모든 태스크 완료 후 1회만 수행 (verification-before-completion-aim에서 처리).
# 서버 종료 후 해당 모듈 테스트만 재빌드/실행
dx tmdown -y
dx bash -c "cd /root/ofsrc/aim/test/unit/gtest/src/<zone>/<module> && make && ./gtest_<module>_<target>"
Confirm:
Then verify production build:
dx make
Test fails? Fix code, not test.
Build fails? Fix now.
After green only:
clang-format -i on changed filesKeep tests green. Don't add behavior.
dx make gtest # still green after refactor
dx make # production build still clean
Next failing test for next feature.
Static functions that need testing must be promoted:
static keyword_helper_func){SOURCE FILE}.h with block comment:/******************************************************************************
* Static Function *
*Although it is a static function, it is declared as follows for unit testing*
******************************************************************************/
int _aim_parse_header(const char *buf, aim_header_t *hdr);
.c file{SOURCE FILE}.h in the .c file소스 트리와 테스트 트리가 1:1 대응한다:
src/<zone>/<module>/<source>.c
→ test/unit/gtest/src/<zone>/<module>/gtest_<module>_<function>.cpp
예시:
src/lib/acp/acp_parser.c
→ test/unit/gtest/src/lib/acp/gtest_acp_acp_parser.cpp
gtest_<module>_<function>.cpp — 함수 1개당 cpp 1개 권장test/unit/gtest/src/<zone>/<module>/핵심 규칙 (test/unit/gtest/AGENTS.override.md 전체 기준 적용):
EXPECT_CALL, Times) 우선Makefile_<target>으로 분리. mock 불필요 테스트는 기본 Makefile의 SOURCES에 추가.c, .h)에 테스트 전용 함수 추가 금지 — static 승격 또는 symbol 가로채기로 해결extern "C" 직접 감싸지 않고, 원본 헤더의 extern "C" guard에 의존커버리지 측정 시 반드시 make clean && make gtest 실행. make gtest-run만 하면 소스 .o가 커버리지 플래그 없이 빌드된 캐시가 남아 .gcda가 생성되지 않음 (커버리지 0%).
gtest 실행 전 dx tmdown -y 필수 — 실행 중인 서버 바이너리가 Text file busy를 유발하여 cp aimdcms 등에서 실패한다. 개별 tmdown -s <server>는 의존 서버(aimidcm 등)가 많아 불완전하므로 전체 종료를 사용한다.
dx tmdown -y
dx bash -c "cd /root/ofsrc/aim && make clean && make gtest"
dx bash -c "cd /root/ofsrc/aim && bash .claude/skills/code-reviewer-aim/scripts/measure_diff_cov.sh"
미커버 라인 식별: gcov를 직접 grep/awk로 파싱하지 말 것. gcov 출력이 메타데이터 5줄만 나오는 재현성 있는 현상이 관찰됨. measure_diff_cov.sh 출력 + dx git diff --unified=0 <base>..HEAD 조합으로 추가 라인을 직접 확인한다.
mock 바이너리가 있는 경우 빌드하지 않고 실행만 추가 (빌드하면 gcda 리셋):
dx bash -c "cd /root/ofsrc/aim/test/unit/gtest/src/<zone>/<module> && ./gtest_<module>_<target>"
80% added-code line coverage required. If below, write more tests.
| Quality | Good | Bad |
|---|---|---|
| Minimal | One thing. "and" in name? Split it. | TEST(Aim, ValidatesEmailAndDomainAndWhitespace) |
| Clear | Name describes behavior | TEST(Aim, Test1) |
| Shows intent | Demonstrates desired API | Obscures what code should do |
"I'll write tests after to verify it works"
Tests written after code pass immediately. Passing immediately proves nothing:
"I already manually tested all the edge cases"
Manual testing is ad-hoc:
"Deleting X hours of work is wasteful"
Sunk cost fallacy. The time is already gone. Working code without real tests is technical debt.
"TDD is dogmatic, being pragmatic means adapting"
TDD IS pragmatic: finds bugs before commit, prevents regressions, documents behavior, enables refactoring. "Pragmatic" shortcuts = debugging in production = slower.
| Excuse | Reality |
|---|---|
| "Too simple to test" | Simple code breaks. gtest takes 30 seconds. |
| "I'll test after" | Tests passing immediately prove nothing. |
| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" |
| "Already manually tested" | Ad-hoc != systematic. No record, can't re-run. |
| "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is technical debt. |
| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. |
| "Need to explore first" | Fine. Throw away exploration, start with TDD. |
| "Test hard = design unclear" | Listen to test. Hard to test = hard to use. |
| "Static function can't be tested" | Promote to header. See AIM-Specific Testing Patterns. |
| "Existing code has no tests" | You're improving it. Add tests for your changes. |
All of these mean: Delete code. Start over with TDD.
Bug: Empty message queue name accepted
RED
TEST(AimMqn, RejectsEmptyQueueName) {
int rc = aim_mqn_validate("");
EXPECT_EQ(rc, AIM_ERR_INVALID_PARAM);
}
Verify RED
$ dx make gtest
[ FAILED ] AimMqn.RejectsEmptyQueueName
Expected: AIM_ERR_INVALID_PARAM
Actual: AIM_OK
GREEN
int aim_mqn_validate(const char *mqn) {
if (mqn == NULL || mqn[0] == '\0') {
return AIM_ERR_INVALID_PARAM;
}
// existing validation...
}
Verify GREEN
$ dx make gtest
[ PASSED ] AimMqn.RejectsEmptyQueueName
$ dx make
Build complete.
REFACTOR
Extract validation for reuse if needed. clang-format -i on changed files.
Before marking work complete:
dx make gtest)dx make)clang-format -i appliedCan't check all boxes? You skipped TDD. Start over.
| Problem | Solution |
|---|---|
| Don't know how to test | Write wished-for API. Write assertion first. |
| Test too complicated | Design too complicated. Simplify interface. |
| External dependency | Use link-time substitution or wrapper function. |
| Test setup huge | Extract test fixtures. Still complex? Simplify design. |
Bug found? Write failing test reproducing it. Follow TDD cycle. Test proves fix and prevents regression.
Never fix bugs without a test.
When adding test utilities or isolating dependencies, read @testing-anti-patterns.md to avoid common pitfalls:
Production code -> test exists and failed first
Otherwise -> not TDD
No exceptions without user's explicit permission.
Called by: