GraphQL 계약 테스트
개요
레이어 간의 이음새에 위치하여 한쪽이 상대방의 동의 없이 움직일 때 깨지는 테스트 — 통제로서의 테스트를 경계에 적용한 것입니다. 핵심 규칙: 계약 — 즉 소비자가 의존하는 데이터 형태와 접근 의미론 — 에 대해 단언하고, 쿼리 문자열 자체에는 절대 단언하지 마십시오. 단순한 쿼리 편집으로 깨지는 테스트는 변경 감지 안티패턴입니다. 보장된 형태나 권한이 움직일 때 깨지는 테스트가 진정한 통제입니다.
§1 — GraphQL 쿼리 계약
- 모든 연산은 알려진 위치에 타입 문서로 한 번 작성되어 내보내지며, 가져와서 사용됩니다. UI가 가져오고, 테스트는 정확히 동일한 아티팩트를 가져옵니다 — 절대 다시 입력한 복사본이 아닙니다. 테스트에 쿼리를 복사하면 계약이 조용히 사라집니다 (UI 쿼리를 편집해도 더 이상 테스트가 깨지지 않음). DRY가 중요한 책임을 지닙니다.
- 설계상 양방향으로 깨집니다:
- DB → UI 드리프트, 컴파일 타임.
graphql-codegen이 라이브 스키마에서 TypeScript 타입을 생성합니다. 사용 중인 필드의 이름을 바꾸거나 제거하면tsc가 모든 사용처 그리고 공유 문서에서 실패합니다 — 사용자가 빈 패널을 보기 전에 빌드가 중단됩니다. 이것이 라이브 스키마에 대한 코드 생성이 필수인 이유입니다. - UI → DB / 권한 드리프트, 런타임. 공유 문서가 시드된 테스트 DB에 대해 실제 호출 경로를 통해 역할 컨텍스트 하에 실행되며, 반환된 형태와 접근 결과에 대해 단언합니다 — 쿼리 텍스트가 아닙니다.
- DB → UI 드리프트, 컴파일 타임.
- 계약 테스트와 RLS 역할 매트릭스는 동일한 수단입니다. 하나의 문서를 PostGraphile을 통해 역할 A(데이터 예상)와 역할 B(거부/빈 값 예상)로 실행합니다. 하나의 메커니즘으로 세 가지를 증명합니다: 쿼리가 여전히 스키마와 일치하는지, UI 가정이 여전히 유효한지, RLS가 여전히 경계를 강제하는지.
실제 사례 — 실제 버그가 어떻게 포착되는지
- DB→UI: 마이그레이션이
equipment.is_active를 제거합니다. Codegen이 새 스키마에서 다시 생성합니다. 생성된 타입이 필드를 잃습니다.tsc가 공유 문서와 해당 필드를 사용하는 모든 컴포넌트에서 실패합니다 — 병합 전 빌드에서 포착됩니다. 어떤 테스트도 "쿼리 텍스트 == X"를 단언하지 않습니다. - UI→DB: 개발자가 UI 쿼리를 확장하여
cancellation_reason을 가져옵니다 (RLS가 높은 역할에만 노출). 일반 멤버로 실행되는 런타임 테스트가 멤버가 볼 수 있는 형태를 단언하다가 이제 null/거부를 받고 실패합니다 — "이것을 노출하려는 의도였나?"를 강제합니다.
§2 — 라우트 계약 및 스모크 테스트
- 라우트는 단일 소스에서 열거됩니다. 각 라우트에는 스모크 테스트가 있습니다: 오류 없이 렌더링 + 최소한의 필수 요소 세트(헤딩, 랜드마크, 주요 컨트롤 — 상세한 상호작용은 아님)를 보여줍니다.
- 라우트를 추가/제거하면 일치하는 테스트 변경이 강제됩니다 — 새 URL은 스모크 테스트 없이 불완전합니다. 제거된 URL은 테스트를 제거합니다. URL이 조용히 나타나거나 사라지지 않습니다.
- 명시적으로 상세한 UI/상호작용 테스트가 아닙니다 — 의도적이고 범위가 제한됩니다. 질문은 "이 URL이 작동하고 필수 요소를 렌더링하는가"이며, 그 이상은 아닙니다.
강제: CI에서 graphql-codegen + tsc (차단)로 컴파일 타임 절반을 처리하고, 시드된 DB에 대한 런타임 계약 + 라우트 스모크 테스트로 나머지를 처리합니다.
전체 근거: 엔지니어링 헌법 제XV조, engineering-constitution/references/engineering-constitution.md에 번들로 포함되어 있습니다.