tdd · diff

git:20260826.34a9b20 to git:20260928.877a6b1

46 added, 34 removed. Audit A to A.

---
name: tdd
description: Test-Driven Development 방법론 스킬
compatibility: omp
---
# TDD (Test-Driven Development) Skill
테스트를 먼저 작성하고 구현하는 RED-GREEN-REFACTOR 사이클을 적용하는 스킬입니다.
## 핵심 원칙
- **테스트 없이 코드를 작성하지 않는다.** 이 규칙을 위반하면 작업을 거부합니다.
+ **테스트 없이 코드를 작성하지 않는다.** 동작이 바뀌는 코드는 그 동작을 정의하는 테스트를 먼저 쓰고, 테스트 없이 동작을 바꾸는 요청은 거부한다. 읽기 전용 조사, 문서, 기계적 리네이밍처럼 관찰 가능한 동작이 바뀌지 않는 작업에는 적용하지 않는다.
+ 좋은 테스트는 **public interface를 통해 동작을 검증**하며 구현 세부를 모른다. 코드가 통째로
+ 바뀌어도 테스트는 바뀌지 않아야 한다. 좋은 테스트는 명세처럼 읽힌다: "유효한 장바구니로 결제할
+ 수 있다"는 어떤 능력이 존재하는지 정확히 말하고, 내부 구조에 무관하므로 리팩토링을 견딘다.
+ 탐색할 때 `CONTEXT.md`(있으면)를 읽어 테스트 이름과 인터페이스 어휘를 도메인 언어에 맞춘다.
+
+ ## Seam: 테스트가 놓이는 자리
+
+ **seam**은 테스트하는 public 경계다 — 안을 들여다보지 않고 동작을 관찰하는 인터페이스.
+ 테스트는 seam에 산다. 내부에 대고 쓰지 않는다.
+
+ **사전에 합의된 seam에서만 테스트한다.** 테스트를 쓰기 전에 테스트 대상 seam을 적고 사용자와
+ 확인한다. 합의되지 않은 seam에는 테스트를 쓰지 않는다. 전부 테스트할 수는 없으므로 seam을
+ 먼저 합의하는 것이 테스트 노력을 모든 엣지 케이스가 아니라 핵심 경로와 복잡한 로직에 놓는
+ 방법이다. 물어라: "public interface가 무엇이고, 어느 seam을 테스트해야 하나요?"
+
+ 인터페이스의 모양 자체가 문제일 때(모듈이 얼마나 깊은지, seam이 어디 놓여야 하는지)는
+ `codebase-design` 스킬의 어휘(module, interface, depth, seam, adapter)를 참조한다.
+
+ ## 안티패턴
+
+ - **구현 결합**: 내부 협력자를 mock하거나, private 메서드를 테스트하거나, 옆 통로(인터페이스
+ 대신 DB를 직접 조회)로 검증한다. 징후: 동작은 그대로인데 리팩토링하면 테스트가 깨진다.
+ - **동어반복**: 단언이 코드와 같은 방식으로 기대값을 다시 계산한다(`assert.Equal(a+b, add(a,b))`,
+ 같은 방식으로 손으로 만든 스냅샷, 상수를 자기 자신과 비교). 구성상 통과하므로 코드와
+ 불일치할 수 없다. 기대값은 독립된 진실 — known-good 리터럴, 손으로 푼 예제, 스펙 — 에서 온다.
+ - **수평 슬라이스**: 테스트를 전부 먼저 쓰고 구현을 전부 나중에 쓴다. 뭉텅이 테스트는 *상상한*
+ 동작을 검증하고, 구현을 이해하기 전에 테스트 구조를 확정한다. 대신 **수직 슬라이스**로 일한다:
+ 테스트 하나 → 구현 하나 → 반복. 각 테스트는 직전 사이클이 가르친 것에 반응하는
+ **tracer bullet**이다.
+
## RED-GREEN-REFACTOR 사이클
### RED 단계: 실패하는 테스트 작성
```
1. 구현하려는 동작을 테스트로 먼저 작성
2. 테스트가 실패하는지 확인 (컴파일 에러 포함)
3. 올바른 실패 이유인지 확인
```
테스트 작성 원칙:
- 테스트는 하나의 동작만 검증
- 이름은 `Test[Subject]_[Scenario]_[Expected]` 형식
- Given-When-Then 구조 사용
- Table-driven tests로 다양한 케이스 커버
```go
func TestCalculate_WithZeroInput_ReturnsError(t *testing.T) {
t.Parallel()
// Given
input := 0
// When
_, err := Calculate(input)
// Then
assert.Error(t, err)
assert.ErrorIs(t, err, ErrInvalidInput)
}
```
### GREEN 단계: 최소 구현으로 테스트 통과
```
1. 테스트를 통과시키는 가장 단순한 코드 작성
2. 과도한 최적화나 일반화 금지
3. 테스트 통과만을 목표로
```
### REFACTOR 단계: 코드 품질 개선
```
1. 중복 제거 (DRY 원칙)
2. 명명 개선
3. 복잡도 감소
4. 테스트는 항상 그린 상태 유지
```
- ## Go 테스트 패턴
+ ## 테스트 작성 패턴
- ```go
- func TestMyFunction(t *testing.T) {
- t.Parallel()
+ 프로젝트 테스트 프레임워크의 관용구를 따른다. table-driven을 지원하는 언어에서는 정상/경계/에러 케이스를 한 테이블에 모으고, 각 케이스 이름이 검증하는 동작을 말하게 한다. 위 RED 예시가 이 형식의 최소 형태다.
- tests := []struct {
- name string
- input int
- want int
- wantErr bool
- }{
- {"정상 입력", 5, 25, false},
- {"영 입력", 0, 0, true},
- {"음수 입력", -1, 0, true},
- }
+ ## 루프의 규칙
- for _, tt := range tests {
- tt := tt
- t.Run(tt.name, func(t *testing.T) {
- t.Parallel()
- got, err := MyFunction(tt.input)
- if tt.wantErr {
- require.Error(t, err)
- return
- }
- require.NoError(t, err)
- assert.Equal(t, tt.want, got)
- })
- }
- }
- ```
+ - **red가 green보다 먼저.** 실패 테스트를 먼저 쓰고, 그것을 통과시킬 만큼만 코드를 쓴다.
+ 미래의 테스트를 예상하거나 투기적 기능을 넣지 않는다.
+ - **한 번에 한 슬라이스.** 사이클마다 seam 하나, 테스트 하나, 최소 구현 하나.
+ - **REFACTOR는 green 뒤에만.** 테스트가 green인 상태에서만 구조를 바꾸고, 리팩토링 중에는
+ 동작을 바꾸지 않는다. 얕은 모듈을 깊게 합칠 때 옛 단위 테스트가 낭비가 되면 지운다
+ (`codebase-design`의 "교체하되 겹치지 않는다").
## 완료 기준
- - [ ] 모든 새 코드에 테스트 존재
- - [ ] 테스트 커버리지 85% 이상
- - [ ] `go test -race ./...` 통과
- - [ ] 각 단계에서 커밋 생성
+ - [ ] 테스트 대상 seam을 사용자와 합의했다
+ - [ ] 변경된 동작마다 그것을 단정하는 테스트가 있다
+ - [ ] 프로젝트 테스트 명령이 통과한다 (동시성 코드는 race 플래그 포함)
+ - [ ] 테스트 커버리지 85% 이상 (프로젝트가 `workflow.coverage_threshold`로 다른 값을 선언하면 그 값)
+
+ Seam 합의·안티패턴·루프 규칙은 mattpocock/skills `tdd`(MIT)에서 가져와 다듬었다.