테스트 하네스 강제

테스트 하네스 강제

테스트가 구조를 우회하면, 테스트는 구조를 검증하지 않는다. 그리고 AI는 테스트 코드를 정본 예시로 참조한다.


두 가지 위험

테스트에서 구조를 생략할 때 생기는 문제는 두 종류다.

첫째, 테스트가 실제 실행 경로를 검증하지 않는다. Provider 없이 렌더링된 테스트는 실제 트리의 동작을 보장하지 않는다. Planner 없이 실행된 테스트는 Planner를 통한 흐름이 맞는지 보장하지 않는다. 테스트가 통과해도 production에서 실패하는 이유가 여기 있다.

둘째, AI가 테스트 코드를 정본 예시로 참조한다. 테스트에서 task.status = "completed"나 direct call이 허용되면, AI는 production 코드에서도 같은 패턴을 복제한다.

이 두 위험은 같은 원인에서 나온다. 테스트에서 경계를 완화하는 것. 해결책도 같다. 테스트에서도 실제 경계가 그대로 작동하게 만든다.


공통 원칙

테스트 하네스 강제는 세 가지를 요구한다.

  • 테스트 helper가 production의 provider와 진입점을 재현한다
  • 상태와 실행은 production과 같은 합법 경로만 통과한다
  • lint와 analyzer가 테스트 파일에도 동일하게 적용된다

문제 1: Provider 없는 렌더링

// 위반
render(<DocumentEditor docId="doc-1" />);

// 합법
renderWithAppProviders(<DocumentEditor docId="doc-1" />, {
  initialState: { documents: [{ id: "doc-1", title: "Test Doc" }] },
});

raw render가 허용되면 Provider 부재가 테스트에서만 조용히 숨겨진다. 프로젝트 전용 helper가 실제 Provider 계층을 재현해야 하고, render 직접 사용은 lint에서 막는 편이 맞다.

관련 사례:


문제 2: 상태 직접 설정

// 위반
createDocument({ status: "published" })
doc.status = "published"

// 합법
const doc = createDocument({ status: "draft" })
submitForReview(doc.id)
publish(doc.id)

테스트에서 상태를 점프하면 전이 경로와 validation, event publish, 후속 처리까지 함께 사라진다. 편의용 생성자, reflection, direct store mutation은 모두 같은 종류의 우회다.

관련 사례:


문제 3: Planner/dispatch 우회

// 위반
saveDocument(doc)
publishDocument(doc)

// 합법
dispatch({ type: "save", docId: "doc-1", content: "Updated" })
dispatch({ type: "publish", docId: "doc-1" })

테스트에서 direct execution을 허용하면 production에서는 닫아 둔 우회 경로를 테스트가 다시 열어 준다. 테스트도 Planner나 dispatch를 통해서만 실행이 시작되어야 한다.

관련 사례:


Fake vs Mock

외부 의존성을 대체할 때는 호출 여부만 확인하는 Mock보다 계약을 실제로 구현하는 Fake가 더 적합하다.

Mock은 “호출됐다"를 검증한다. Fake는 “이 경로가 실제로 같은 계약을 통과하는가"를 검증한다. 테스트 하네스 강제의 목적은 경계 보존이지 호출 횟수 수집이 아니다.

Fake를 쓴다면 global override보다 helper나 provider를 통해 주입하는 편이 맞다. 그래야 테스트에서도 실제 조립 경로가 유지된다.


Storybook도 같은 경계를 따라야 한다

Storybook story는 문서이면서 예시다. Provider 없는 story, direct data injection, direct executor call이 story에 남아 있으면 AI는 그것도 허용된 사용법으로 학습한다.

테스트와 Storybook은 같은 helper와 같은 provider 조합을 공유하는 편이 안전하다. story에서만 다른 경계를 쓰기 시작하면 문서 surface가 우회 surface로 바뀐다.


체크리스트

  • renderWithAppProviders 같은 프로젝트 전용 helper가 존재하는가?
  • raw render 직접 사용이 lint로 차단되는가?
  • 모든 테스트가 실제 Provider 계층 안에서 실행되는가?
  • 상태 직접 설정과 direct store mutation이 금지되어 있는가?
  • Planner나 dispatch를 우회하는 실행이 테스트에서도 차단되는가?
  • Fake가 Mock보다 우선적으로 사용되는가?
  • Fake가 global override가 아니라 helper 또는 provider를 통해 주입되는가?
  • Storybook이 테스트와 동일한 경계를 공유하는가?
  • 빌드 타임 규칙이 테스트 파일에도 동일하게 적용되는가?