테스트 — 제어 표본(Control)
개요
테스트는 제어 표본입니다. 즉, 기대값을 고정시켜 행동의 변화가 눈에 띄게 만듭니다. 핵심 규율은 테스트를 코드에 맞추지 말고, 왜 서로 다른지 이해하라는 것입니다.
규칙
-
동작 변경은 테스트 우선(Test-first). 실패하는 테스트를 먼저 작성하고, 올바른 이유로 실패하는지 확인한 후, 통과하는 최소한의 코드를 작성하십시오. 실패를 보지 않았다면, 당신이 생각한 대로 테스트하는지 알 수 없습니다. 이 루프 자체는
superpowers:test-driven-development스킬에서 다루며, 이 스킬은 테스트가 무엇을 포함해야 하는지와 일단 테스트가 존재할 때 실패가 무엇을 의미하는지를 규율합니다. 해당 스킬이 설치되었는지 여부와 관계없이 이 규칙은 유효합니다. -
구조적 변경은 변경되지 않은 스위트로 검증합니다. 순수 구조적 변경(이름 변경, 이동, 분할, 추출 — 관찰 가능한 동작 변화 없음)에는 새 테스트가 필요하지 않습니다. 그 관문은 반대입니다. 기존 전체 스위트가 수정된 테스트 없이 모두 통과해야 합니다. 이것이 테스트 우선이 여기에 적용되지 않는 이유입니다 — 지정할 동작이 없으므로 어떤 테스트도 의미 있게 먼저 실패할 수 없습니다. 구조적 변경이 테스트 편집을 강제한다면, 그것은 구조적 변경이 아니었습니다. 멈추고 다음 규칙을 사용하여 재분류하십시오.
-
계약 경계를 넘는 이름 변경은 리팩터링이 아닙니다. 핵심 질문은 "코드가 동작을 바꾸었는가?"가 아니라 "이 이름이 내가 제어하지 않는 무언가에게 관찰 가능한가?"입니다.
변경된 대상 분류 관문 내부 심볼, 비공개 헬퍼, 로컬 파일 구조적 기존 스위트가 수정 없이 통과 내보낸 함수, API/GraphQL 필드, 라우트, CLI 플래그, DB 컬럼, 이벤트 이름, 설정 키 동작적 — 소비자에게 영향 테스트 우선 + 추가 후 폐기 공개 이름 변경은 동일한 데이터에 동일한 작업을 수행하지만 여전히 호환성을 깨뜨리는 변경입니다. 이름이 계약이었기 때문입니다(히럼의 법칙 —
performance-and-scale참조). 동작적 변경으로 취급하십시오: 우선 실패하는 테스트 사이클로 새 이름을 추가하고, 이전 이름과 그 통과 테스트를 폐기 기간 동안 유지하며, 소비자가 마이그레이션한 후에만 이전 이름을 제거하십시오.세 번째 사례는 이름을 문자열로 참조하는 테스트입니다 — 스냅샷, DI 컨테이너 키, 리플렉션, 픽스처 경로 등. 이러한 것들은 내부 이름 변경 시 깨집니다. 이는 규칙이 실패한 것이 아니라, 테스트가 구현 세부 사항에 결합되었음을 알리는 신호입니다. 결합을 수정하고, 다시 기준을 맞추지 마십시오.
-
리팩터링은 테스트를 수정해서는 안 되며, 요구사항 변경만 수정해야 합니다. 서로 다른 행위입니다:
- 리팩터링이 테스트를 깨뜨렸다 → 테스트가 의도하지 않은 동작 변경을 포착한 것입니다. 멈추고 이해하십시오. 리팩터링을 통과시키기 위해 테스트를 수정하는 것은 계측을 파괴하는 행위입니다.
- 요구사항이 실제로 변경되었다 → 테스트를 의도적으로, 별도의 검토 가능한 변경으로 업데이트하고, 그렇게 기록하십시오.
- 금지된 행위는 조용히 기준을 다시 맞추는 것입니다.
-
대칭적 커버리지 — 모든 규칙을 양방향으로 테스트하십시오. 인가된 주체가 할 수 있음(allow) 을 증명하고, 인가되지 않은 주체가 할 수 없음(deny) 을 증명하십시오. 해피 패스만 테스트하는 것은 규칙의 절반만 테스트하는 것입니다.
-
권한/역할 규칙은 매트릭스입니다: 모든 의미 있는 (역할 × 액션 × 리소스) 셀에 대해 허용(allow)과 거부(deny) 를 테스트하십시오. 새로운 역할은 거부(deny) 사례가 존재할 때까지 불완전합니다. 갑자기 통과하게 된 거부 테스트는 보안 회귀(security regression) 입니다 — 명시적으로 검토하고 수용하며, 절대 기준을 다시 맞추지 마십시오.
-
진입점(Entry points)은 조용히 추가/제거되지 않습니다. 라우트, 명령어, 엔드포인트 각각에 존재 여부와 스모크 커버리지를 포함하여 추가/제거가 테스트 변경을 강제하도록 하십시오.
-
크기별 테스트: 소형(small) (순수 로직, I/O 없음, 결정적), 중형(medium) (실제 DB/프로세스, 격리된 환경), 대형(large) (전체 스택). 많은 소형, 적은 중형, 가장 적은 대형. 누락된 중간(missing middle) 을 주시하십시오 — 서버/애플리케이션 로직이 단위 테스트나 통합 테스트 모두 다루지 않는 부분으로, 각 계층이 다른 계층이 테스트한다고 가정하기 때문입니다.
-
불안정한 테스트(Flaky tests)는 발견 즉시 격리합니다. 코드 변경 없이 통과/실패하는 테스트는 전체 스위트에 대한 신뢰를 파괴합니다. 즉시 격리하십시오(차단 게이트에서 제외하고, 수정을 위한 이슈 등록) — 간헐적으로 실패하게 두지 말고, 조용히 삭제하지 마십시오. 소형 테스트는 결정적이어야 합니다: 실제 시계, 무작위성, 네트워크가 없어야 합니다.
-
테스트 코드는 DAMP이지 DRY가 아닙니다. Descriptive And Meaningful Phrases(설명적이고 의미 있는 구문)가 중복 제거보다 우선합니다. 테스트는 분리되어 읽혀도 명확해야 합니다. 테스트에 DRY를 적용하는 것은 위반이지 미덕이 아닙니다(
change-hygiene-and-code-craft참조).
일반적인 합리화
| 변명 | 현실 |
|---|---|
| "코드를 먼저 작성한 다음 테스트를 추가하겠습니다" | 그러면 테스트가 요구사항이 아닌 코드에 맞춰진 것입니다. 실패하는 것을 본 적이 없으므로, 그것이 무엇을 테스트하는지 알 수 없습니다. |
| "단순한 이름 변경이라 테스트가 필요 없습니다" | 맞습니다 — 이름이 내부적이라면 말이죠. 내보내졌거나 공개된 경우, 이름이 계약이었으며 이름 변경은 동작적 변경입니다. |
| "이름을 바꾸고 세 개의 테스트를 수정해야 했는데, 정상입니다" | 정상이 아닙니다. 계약 경계를 넘었거나, 해당 테스트들이 구현 세부 사항에 결합되어 있었습니다. 재분류하십시오. |
| "테스트가 이제 틀렸으니 업데이트만 하겠습니다" | 요구사항이 변경된 경우에만 해당됩니다. 리팩터링 중이라면 테스트가 실제 변경을 포착한 것입니다 — 조사하십시오. |
| "해피 패스를 테스트했으니 충분합니다" | 거부 사례도 테스트될 때까지 규칙은 테스트된 것이 아닙니다. |
| "이 거부 테스트가 이제 통과하네요, 좋아요, 배포합니다" | 금지된 동작이 성공하는 것은 보안 회귀이지 진전이 아닙니다. |
| "테스트가 불안정하니 CI를 다시 실행하세요" | 불안정성은 실제 버그를 감추고 모두가 실패를 무시하는 습관을 들입니다. 격리하고 수정하십시오. |
| "공유 헬퍼로 테스트들을 DRY하게 만들겠습니다" | 테스트는 DAMP입니다. 제자리에서 명확성을 돕는 중복은 유지됩니다. |
위험 신호 — STOP
- 동작 변경에 대한 프로덕션 코드를 실패하는 테스트 없이 작성하는 경우
- 구조적 변경의 diff가 테스트 파일도 건드리는 경우
- 공개/내보낸 이름을 추가 후 폐기 대신 제자리에서 변경하는 경우
- 리팩터링 중 테스트를 통과시키기 위해 수정하는 경우
- 거부 사례 테스트가 없는 새 권한
- 스냅샷/단언(assertion)을 왜 변경되었는지 이해하지 않고 다시 기준을 맞추는 경우
- 실제
sleep,Date.now(), 무작위 데이터, 또는 네트워크 호출이 있는 테스트 - 가독성을 희생하며 중복을 제거하기 위해 테스트 코드를 리팩터링하는 경우
전체 근거: 헌법 제4조, engineering-constitution/references/engineering-constitution.md에 번들로 제공됩니다. GraphQL/라우트 계약 세부 사항은 graphql-contract-testing을 참조하십시오.