Files
clean-architecture-frontend…/docs/testing/frontend-platform-testing-strategy.md
T

42 KiB

프론트엔드 플랫폼 테스트 전략

이 문서는 도메인 기능을 추가하기 전과 추가한 후에 프론트엔드 플랫폼의 위험을 어느 테스트 계층에서 차단할지 정의한다. 목표는 테스트 개수를 늘리는 것이 아니라, 실패 원인과 가장 가까운 계층에서 빠르고 결정적으로 결함을 발견하고 실제 브라우저·실제 production build에서만 드러나는 위험을 별도 게이트로 닫는 것이다.

이 문서는 다음 공식 문서를 기술 기준으로 사용한다.

1. 테스트 원칙

  1. 테스트는 구현 계층이 아니라 사용자·운영 위험에서 출발한다.
  2. 같은 위험을 모든 계층에서 반복해 검증하지 않는다.
  3. pure policy와 codec은 빠른 unit/contract test에서 가능한 모든 분기를 닫는다.
  4. 브라우저 API, React lifecycle, focus, layout은 DOM 또는 실제 브라우저에서 검증한다.
  5. HTTP client를 stub하는 대신 네트워크 경계는 MSW로 검증한다.
  6. production boot와 route는 개발용 component fixture만으로 완료 처리하지 않는다.
  7. 성공 경로만으로 완료하지 않는다. timeout, abort, schema mismatch, 권한, 충돌, 재시도 소진을 포함한다.
  8. 접근성 자동 검사와 시각 회귀는 서로 대체하지 않는다.
  9. coverage 숫자는 누락 탐지 신호이며 behavior 완료 증거가 아니다.
  10. flaky test를 자동 재시도로 숨기거나 skip 상태로 무기한 유지하지 않는다.
  11. test-only 우회 경로와 mock credential을 production bundle에 포함하지 않는다.
  12. 모든 CI gate는 실패 시 종료 코드가 non-zero여야 하며 continue-on-error로 낮추지 않는다.

2. 현재 기준선

현재 저장소는 테스트 계층과 CI 증거 경로를 명시적으로 분리한다.

2.1 실행 명령

package.json에는 다음 계층이 있다.

  • test:runtime-schema
  • test:unit
  • test:component
  • test:integration
  • test:e2e
  • test:a11y
  • review:a11y-manual
  • test:sample-removal
  • test:performance

Vitest 결과는 JUnit으로 artifacts/tests/에 기록되고 Playwright는 HTML report, trace, failure screenshot을 보존한다. config/ci/gates.json은 품질 gate와 증거 경로를 등록하며 .gitea/workflows/quality-gates.yml은 merge/release/production/ field/documentation 단계를 구성한다.

2.2 확인된 강점

  • runtime config와 release manifest schema test가 분리되어 있다.
  • retry, error classification, compatibility, performance policy 같은 pure logic에 unit test가 있다.
  • React Testing Library와 user-event로 component behavior를 검사한다.
  • MSW Node server로 HTTP 경계를 실제 fetch 수준에서 격리한다.
  • 잘못된 architecture/security/type fixture가 실제로 거절되는지 검증한다.
  • Chromium, Firefox, WebKit 프로젝트가 구성되어 있다.
  • 등록된 모든 route에 axe 검사가 있다.
  • 320px reflow와 mobile navigation을 E2E로 확인한다.
  • release build의 bundle과 lab performance budget이 별도 gate다.

2.3 확인된 공백

테스트 TypeScript typecheck 기반

check:types는 app, Node scripts/config, tests project를 순서대로 검사한다. 테스트 project는 JS/JSX/TS/TSX의 callback, mock, fixture와 config type을 검사하되 실패를 의도한 tests/fixtures는 별도 negative command가 소유한다. Vitest의 변환 성공을 TypeScript typecheck의 대체물로 취급하지 않는다.

실제 bootstrap integration test가 없다

tests/component/bootstrap-shell.test.jsx는 production bootstrap을 import하지 않고 테스트 내부의 <TestShell>만 렌더링한다. E2E는 실제 entry를 통과하지만, 다음 실패를 작은 통합 테스트에서 식별하기 어렵다.

  • runtime config fetch 실패
  • config/manifest mismatch
  • adapter composition 실패
  • provider 순서 또는 누락
  • external auth owner 유무
  • product tree를 마운트하기 전 fail-closed
  • boot error shell의 safe metadata
  • StrictMode와 unmount cleanup

TanStack Query의 React integration test 기반

tests/component/application-query.test.jsx는 production query inbound adapter의 query/mutation lifecycle을 검증한다. cancellation, initial terminal failure, background stale-failure latch와 retry 복구, duplicate submit, optimistic commit/rollback, conflict 해제와 namespace invalidation이 실제 QueryClient 위에서 실행된다. HTTP 자동 retry가 소유자이므로 이 adapter의 query/mutation vendor retry는 꺼져 있다.

Form 테스트가 단일 TextField 흐름에 머문다

현재 component/E2E는 label, description, error association과 빈 값 submit을 검사한다. error summary, 첫 오류 focus, async validation race, 422 field error, double submit, dirty navigation, mutation conflict는 없다.

Route registry와 실행 tree가 별도로 테스트된다

registry snapshot과 일부 navigation/access policy test는 있으나 다음 계약을 강제하지 않는다.

  • 모든 route ID에 lazy runtime module이 존재하는가
  • params/search가 실제 codec으로 검증되는가
  • deep link와 basename refresh가 동작하는가
  • chunk load failure가 1회 reload/support surface로 연결되는가
  • route error boundary가 location 변경 시 reset되는가
  • scroll restoration과 form navigation blocker가 동작하는가

Storybook과 시각 회귀가 없다

/examples/ui는 통합 gallery지만 component별 모든 state를 격리하지 않는다. Storybook story, interaction story, story-level axe, toHaveScreenshot() baseline이 없다. screenshot: "only-on-failure"는 디버깅 증거이며 시각 회귀 테스트가 아니다.

MSW scenario가 공유되지 않는다

integration file마다 setupServer, handler, response body를 다시 정의한다. Node integration, Storybook browser, feature component test, E2E mock service가 같은 시나리오 이름과 contract fixture를 공유하지 않는다.

E2E가 개발 서버를 대상으로 한다

현재 Playwright web server는 pnpm dev다. route behavior 확인에는 유효하지만 다음 release 위험은 production build/preview에서만 확인할 수 있다.

  • hashed lazy chunk
  • source 변환과 tree shaking
  • base path
  • deep-link fallback
  • build-time environment
  • release manifest와 runtime config 조합
  • minified code의 chunk failure

Coverage가 실행·차단되지 않는다

vitest.config.js에는 reporter만 선언되어 있고 coverage provider, script, threshold, diff policy가 없다.

3. 위험 기반 테스트 계층

우선순위 의미:

  • P0: 오류가 발생하면 애플리케이션 부팅, 데이터 무결성, 인증, 주요 사용 흐름, 보안 또는 접근성이 깨지는 위험
  • P1: 주요 기능 품질, 브라우저 호환성, 운영 복구와 개발 생산성을 저하하는 위험
  • P2: 고급 기능 또는 특정 제품 profile에서만 필요한 위험
위험 우선순위 가장 가까운 테스트 추가 증거
runtime config/manifest mismatch P0 schema + bootstrap integration built-dist smoke
secret-like client config P0 negative contract fixture browser security gate
HTTP request/response schema mismatch P0 adapter integration + MSW feature integration
중복 retry 또는 unsafe mutation retry P0 unit policy + MSW integration E2E degraded scenario
401 복구 loop P0 auth/HTTP integration protected route E2E
query cancellation/stale state 오류 P0 query hook integration route E2E
optimistic mutation rollback 오류 P0 mutation integration visual state smoke
form validation/422 mapping 오류 P0 form component integration route E2E
route access/deep-link 오류 P0 route contract/component built-dist E2E
lazy chunk 복구 loop P0 use-case unit + release E2E runbook drill
keyboard/focus/accessible name 오류 P0 component + story a11y E2E/manual
theme/token contrast 오류 P0 token contract + axe visual/manual
responsive overflow P1 component layout + E2E visual
브라우저 엔진 차이 P1 3-engine E2E manual device smoke
visual regression P1 pinned Chromium screenshot reviewer approval
i18n/RTL/long text clipping P1 story + visual route smoke
telemetry redaction P0 unit/adapter integration bootstrap smoke
bundle 증가 P1 build manifest budget release gate
render/performance 저하 P1 component profiler 선택 lab/field metrics
virtualized collection P2 browser component large-data E2E
offline/PWA persistence P2 service-worker integration installable build E2E

4. TypeScript 테스트 계약

TypeScript 전환 후 production과 test typecheck를 분리하되 둘 다 merge gate로 차단한다.

4.1 목표 구성

tsconfig.base.json
tsconfig.app.json
tsconfig.node.json
tsconfig.test.json
tsconfig.json          # project references

tsconfig.test.json에는 다음을 포함한다.

  • tests/**/*.ts
  • tests/**/*.tsx
  • src/**/*.stories.ts
  • src/**/*.stories.tsx
  • tests/setup.ts
  • custom matcher type
  • MSW handlers와 factories
  • Playwright tests는 필요하면 별도 tsconfig.e2e.json

권장 명령:

tsc --noEmit -p tsconfig.app.json
tsc --noEmit -p tsconfig.test.json
tsc --noEmit -p tsconfig.e2e.json

4.2 Type-level test

다음은 runtime test가 아니라 compile-time test로 닫는다.

  • route ID와 runtime route map의 exhaustive mapping
  • routePath params 누락과 잘못된 key
  • query key factory input/output
  • component variant closed union
  • form schema와 default value shape
  • use case command/result
  • public port 구현 누락
  • vendor type이 public facade 밖으로 새는 문제

필요하면 Vitest의 expectTypeOf를 사용하되 실제 tsc도 실행한다. 의도적으로 실패해야 하는 fixture는 독립 tsconfig@ts-expect-error 설명을 사용한다. 부정 fixture가 실수로 통과하면 gate가 실패해야 한다.

4.3 금지 사항

  • test file을 any로 우회
  • as unknown as로 mock contract를 무조건 통과
  • production type error를 test mock에서 숨김
  • test는 실행되므로 typecheck도 된다고 가정
  • 의도와 설명 없는 @ts-ignore

5. Unit과 Contract 테스트

5.1 Unit 대상

DOM과 network 없이 결정 가능한 로직은 unit test로 검증한다.

  • error classification
  • retry eligibility, attempt cap, backoff와 jitter bound
  • query key canonicalization
  • route access decision
  • params/search codec
  • locale formatter wrapper
  • token/variant static map
  • async view-state reducer
  • form DTO mapper
  • optimistic patch/rollback reducer
  • chunk recovery decision
  • redaction
  • performance/readiness formula

시간, random, UUID는 주입하거나 fake timer로 통제한다. 실제 setTimeout을 기다리는 test를 만들지 않는다.

5.2 Contract 대상

registry와 외부 boundary의 안정된 shape를 검증한다.

  • runtime config schema
  • release manifest schema
  • API operation registry
  • request/response schema registry
  • error registry
  • route registry/runtime map
  • storage key registry
  • telemetry event registry
  • design token registry
  • mock scenario registry

inline snapshot은 사람이 검토 가능한 작고 안정된 registry에만 사용한다. 큰 payload, DOM tree, CSS 전체를 snapshot으로 고정하지 않는다.

5.3 Negative fixture

positive test만으로 control을 증명하지 않는다.

tests/fixtures/
  architecture/
    allowed/
    forbidden/
  security/
    allowed/
    forbidden/
  typecheck/
    allowed/
    forbidden/
  routes/
    malformed-search/
  contracts/
    malformed-envelope/

부정 fixture는 실제 production validator와 동일한 entry로 실행한다.

6. 실제 Bootstrap Integration

bootstrap test는 production entry의 로직을 재작성하지 않는다. main.tsx는 DOM 탐색과 최종 호출만 남기고, 테스트 가능한 함수로 boot를 추출한다.

목표 API 예:

bootstrapApplication({
  rootElement,
  fetcher,
  host,
  buildConfig,
  createRoot,
});

production은 실제 dependency를 넘기고 integration test는 deterministic dependency를 넘긴다.

6.1 필수 시나리오

시나리오 기대
valid config + coherent manifest product tree가 한 번 마운트
config fetch network failure product tree 미마운트, safe boot shell
config non-JSON safe code만 표시
forbidden secret-like key fail-closed
config schema mismatch issue detail/endpoint/body 비노출
manifest fetch/shape failure safe release failure
config/build/release mismatch product tree 미마운트
external auth owner 존재 external session adapter 선택
external auth owner 누락 integration-failed session
local demo auth local/development에서만 허용
telemetry disabled network/queue side effect 없음
storage unavailable memory/failure policy대로 boot
StrictMode subscription/cleanup leak 없음
unmount listener, timer, pending request 정리

6.2 Provider 조립 검증

현재 production provider 순서를 사실대로 검증한다.

QueryClientProvider
  -> BrowserRouter
    -> ThemeProvider
      -> SessionProvider
        -> AppShell

각 provider를 mock component로 대체해 문자열 순서만 검사하지 않는다. 실제 Context/hook을 소비하는 작은 probe route를 렌더링해 provider가 사용 가능한지 확인한다.

application input facade를 도입한 목표 순서는 다음과 같다.

QueryClientProvider
  -> ApplicationProvider
    -> RouterProvider
      -> ThemeProvider
        -> SessionProvider
          -> AppShell

RP-04에서 RouterProvider 기반 Data Mode로 전환했다. router component test와 runtime composition test는 production AppRouter와 composition 함수를 사용하며, 문서에 적힌 provider 순서를 테스트 전용 shell로 재현하지 않는다.

6.3 Boot E2E

bootstrap integration이 통과해도 다음은 production build smoke로 다시 확인한다.

  • /config.json no-store fetch
  • /release-manifest.json coherence
  • base path
  • hashed asset reachability
  • boot error shell
  • public route
  • protected route

7. Query와 Mutation 테스트

TanStack Query를 canonical React inbound adapter 뒤에 연결한 뒤 다음 계층을 제공한다.

Page / AsyncSurface / Form pattern
  -> local query/mutation hook
  -> TanStack Query lifecycle
  -> application input use case
  -> outbound gateway

7.1 Query 필수 시나리오

  • 최초 pending은 loading surface를 표시한다.
  • 성공 payload는 view model로 렌더링된다.
  • 빈 array와 빈 page는 empty로 분류된다.
  • background refetch 중 기존 data를 유지한다.
  • stale + degraded 상태를 표시한다.
  • 같은 query key 요청은 deduplicate된다.
  • filter 순서가 달라도 canonical key가 같다.
  • route unmount 또는 superseded input에서 request를 abort한다.
  • aborted request는 terminal error나 telemetry failure로 오분류되지 않는다.
  • offline/paused와 loading을 구분한다.
  • 401 복구는 한 번만 수행한다.
  • 재로그인 후 허용된 operation만 다시 실행한다.
  • schema mismatch는 safe terminal failure다.
  • retryable failure만 bounded retry한다.
  • query error reset 후 다시 성공할 수 있다.

각 test는 새로운 QueryClient를 생성하고 retry를 명시적으로 통제한다. singleton cache를 test 사이에 공유하지 않는다.

7.2 Mutation 필수 시나리오

  • submit은 한 번만 전송된다.
  • keyed mutation은 동일 logical action에서 idempotency key를 유지한다.
  • 성공 후 올바른 query namespace만 invalidate한다.
  • optimistic update와 success reconcile이 일치한다.
  • 실패 시 이전 cache로 rollback한다.
  • 409 conflict를 일반 500과 구분한다.
  • 422 field error는 safe field map으로 전달한다.
  • non-idempotent mutation은 자동 재시도하지 않는다.
  • unmount 후 state update warning이 없다.
  • 사용자 취소와 timeout을 구분한다.
  • mutation pending 중 중복 action이 차단된다.

7.3 Test helper

tests/support/query/
  create-test-query-client.ts
  render-with-query.tsx
  wait-for-query-idle.ts

helper는 production default를 복사하지 않는다. production factory를 호출하고 필요한 시간·retry만 test override로 주입한다.

RP-05의 executable reference matrix는 tests/features/reference-feature에 모여 있다. 설치 모드에서는 URL codec과 query key/HTTP search의 동일성, DTO 차단, loading/success/empty/terminal, refreshing/stale, pending/duplicate/optimistic/conflict/rollback과 MSW production composition을 검증한다. 제거 모드는 feature source/tests와 installed contract/runtime/adapter contribution을 제거한 복제본에서 P0 gate와 built asset 잔여 0개를 다시 검증한다.

RP-06 form/page matrix는 tests/component/form-foundation.test.tsx, tests/component/page-templates.test.tsx, tests/features/reference-feature/reference-page.test.tsxtests/e2e/reference-form.spec.js에 있다. client validation에서 command 0회와 첫 오류 focus, Zod transform/default, pending 중 중복 제출, 승인된 422 field/unknown field mapping, conflict 입력 보존, reset/dirty, navigation confirmation/focus restore, URL/storage 비노출과 320px reflow를 검증한다. HTTP integration test는 backend copy를 버리고 422 path/code만 전달하는지 별도로 검사한다.

8. Form 테스트

Form test는 field primitive, form controller, application command mapping을 분리한다.

8.1 Field primitive

  • visible label과 accessible name
  • description/error aria-describedby
  • aria-invalid
  • required/readonly/disabled
  • ref와 focus
  • autocomplete/inputmode
  • IME composition
  • controlled/uncontrolled

8.2 Form controller

  • default value
  • touched/dirty
  • submit validation 시점
  • error summary
  • 첫 invalid field focus
  • async validation race와 취소
  • server 422 field/global error
  • pending/double submit
  • reset과 server value reinitialize
  • unsaved navigation blocker

8.3 Application 경계

  • UI string을 command DTO로 normalize
  • 빈 문자열/null/undefined 정책
  • locale number/date parse
  • client validation 통과 후에도 application/domain validation 수행
  • server field name을 승인된 UI field에만 매핑
  • 알 수 없는 server path는 form-level safe error로 이동
  • raw backend message를 직접 표시하지 않음

8.4 E2E 최소 흐름

  1. keyboard만으로 field를 이동한다.
  2. 빈 submit 후 error summary로 focus가 이동한다.
  3. summary link로 field에 이동한다.
  4. 값을 수정한다.
  5. mutation pending을 확인한다.
  6. success 또는 conflict/422를 확인한다.
  7. success 후 dirty guard가 해제된다.

9. Router 테스트

9.1 Contract

  • route ID는 unique하다.
  • path와 navigation order가 unique하다.
  • 모든 route ID에 runtime module이 있다.
  • 모든 runtime module은 registry route ID를 참조한다.
  • params/search codec이 실제 객체다.
  • loading/error surface가 등록된 component key다.
  • route title은 typed message key다.

9.2 Component/integration

  • public route
  • session-required route
  • recovery-pending route
  • integration-failed route
  • forbidden result
  • not-found route
  • params success/failure
  • search default/invalid/canonical serialization
  • same-page query change
  • route heading focus
  • document title
  • scroll restoration
  • redirect loop guard
  • dirty form blocker
  • error boundary reset

9.3 Built-dist E2E

  • direct deep link
  • browser refresh
  • basename 배포
  • back/forward
  • lazy route navigation
  • chunk request failure
  • old HTML/new manifest mismatch
  • 1회 reload guard
  • 404 fallback

client route guard는 UX이며 authorization이 아님을 test 이름과 문서에서 유지한다. 403은 server 결과로도 별도 검증한다.

10. Design System 테스트

공용 컴포넌트는 한 번의 결함이 모든 페이지로 전파되므로 위험도가 높다.

RP-07의 실행 경로는 check:design-system, check:design-system:fixture, check:types:fixture:icon-button, tests/component/design-system-platform.test.tsxtests/e2e/design-system-interactions.spec.js다. Story interaction/visual baseline은 VD-08/RP-10에서 추가하며 현재 runtime gallery를 isolated workshop 완료 증거로 사용하지 않는다.

10.1 Component behavior

  • native role/name/value
  • keyboard matrix
  • focus-visible
  • disabled/readonly/pending
  • controlled/uncontrolled
  • ref
  • callback 횟수와 payload
  • portal cleanup
  • overlay focus trap/restore
  • live-region announcement
  • reduced motion

class 이름 자체보다 사용자에게 관찰 가능한 behavior를 우선한다. variant class map의 exhaustive 여부는 unit/type test에서 별도로 검사한다.

10.2 Token contract

  • 필수 semantic token 존재
  • light/dark 모두 정의
  • component가 raw 색상 사용하지 않음
  • focus/status contrast
  • forced-colors fallback
  • 반복 arbitrary value 차단
  • token gallery와 registry 일치

10.3 Story matrix

모든 primitive/pattern은 다음 story를 가진다.

  • default
  • variants
  • disabled/readonly
  • pending/loading
  • invalid/error
  • long text
  • compact
  • dark
  • pseudo-locale
  • RTL

overlay는 open, keyboard, outside dismiss, long content, nested content를 추가한다. form은 client/server error와 async pending을 추가한다.

10.4 접근성

Storybook a11y를 기본 error로 설정한다. route axe가 초기 화면만 검사하는 공백을 open dialog, invalid form, expanded menu 같은 state story로 보완한다.

자동 axe 결과가 없더라도 다음을 component behavior로 직접 검사한다.

  • focus order
  • focus trap/restore
  • keyboard shortcut
  • live announcement의 중복
  • visible focus

11. Shared MSW Scenario Catalog

MSW는 request client를 mock하지 않고 HTTP 경계에서 요청을 가로챈다. Node integration과 browser workshop이 동일한 operation contract 및 scenario vocabulary를 사용하도록 중앙 catalog를 만든다.

11.1 목표 구조

tests/mocks/
  contracts/
    envelopes.ts
    payloads.ts
  handlers/
    runtime-config.ts
    release-manifest.ts
    resources.ts
  scenarios/
    catalog.ts
    create-scenario-handlers.ts
  server.ts
  browser.ts

tests/factories/
  failure-factory.ts
  resource-factory.ts
  session-owner-factory.ts

11.2 공통 scenario ID

ID 응답
success 유효 envelope와 data
empty 유효한 빈 collection
slow 통제된 지연
network-error transport 실패
timeout client timeout 초과
aborted navigation/user/superseded abort
content-type-mismatch JSON이 아닌 content type
malformed-json 파싱 불가능한 JSON
envelope-mismatch top-level contract 위반
schema-mismatch data payload contract 위반
auth-recover-once 첫 401 후 복구 성공
auth-persistent-401 복구 뒤에도 401
forbidden-403 권한 없음
not-found-404 리소스 없음
conflict-409 mutation conflict
validation-422 승인된 field/global error
rate-limited-429 Retry-After 포함
server-retry-success bounded retry 뒤 성공
server-terminal-500 retry 소진

11.3 Catalog 규칙

  • scenario ID는 typed closed set이다.
  • operation별 허용 scenario를 명시한다.
  • factory default는 고정 값이며 random에 의존하지 않는다.
  • timestamp/UUID가 필요하면 seeded factory를 사용한다.
  • raw fixture에는 실제 개인정보·token을 넣지 않는다.
  • test가 override한 handler는 afterEach에서 reset한다.
  • 처리되지 않은 request는 onUnhandledRequest: "error"로 실패한다.
  • test가 expected request count와 중요한 header/body를 검증한다.
  • retry test는 attempt counter를 test 내부에서 초기화한다.
  • scenario response는 production schema validator도 통과하거나 의도적으로 실패해야 한다.

11.4 환경별 사용

Vitest Node
  -> setupServer(...handlers)

Storybook
  -> setupWorker(...handlers)

Playwright release E2E
  -> shared scenario catalog로 생성한 local mock API
     또는 동일 fixture builder를 사용하는 page.route

MSW browser worker와 scenario selector는 development/test entry에서만 import한다. production runtime config에 MOCK_MODE, SCENARIO, fake token 같은 key를 추가하지 않는다.

12. Storybook 테스트

Storybook은 component를 격리해 state를 열거하고 실제 브라우저 interaction과 접근성을 검사하는 개발·CI surface다. /examples/ui는 앱 통합 smoke로 유지하되 Storybook을 대신하지 않는다.

12.1 Global decorator

  • semantic token stylesheet
  • ThemeProvider
  • LocaleProvider
  • Memory Router
  • SessionProvider test owner
  • QueryClientProvider
  • MSW browser worker
  • portal root

모든 story가 production provider와 다른 임의 wrapper를 만들지 않도록 renderStory helper를 하나만 둔다.

12.2 Interaction test

Storybook play 함수는 다음에 사용한다.

  • menu/dialog/drawer 열기와 닫기
  • keyboard navigation
  • form validation
  • toast dismiss
  • tab/pagination selection
  • optimistic mutation state

같은 세부 behavior를 RTL unit과 story에 모두 복사하지 않는다. pure DOM behavior는 빠른 component test, 여러 provider/network/browser interaction은 story에 둔다.

12.3 Story a11y

  • 기본 policy는 error
  • rule disable은 component/story별 owner, 사유, 만료일 필요
  • critical/serious만이 아니라 목표 WCAG tag의 모든 violation을 검토
  • incomplete 결과는 수동 검토 목록으로 전달

12.4 Story build

build-storybook은 merge gate다. 다음을 차단한다.

  • 깨진 import
  • 누락된 provider
  • vendor facade 교체 오류
  • story type error
  • public component가 isolated build에서만 실패하는 문제

Storybook static artifact를 production application artifact와 합치지 않는다.

13. Playwright 전략

13.1 두 실행 profile

개발 피드백

  • Vite dev server
  • 빠른 Chromium 중심 smoke
  • 로컬 reuseExistingServer
  • 개발 중 route와 form 확인

Release 검증

pnpm build
  -> pnpm preview
  -> Playwright release config
  • CI에서 기존 server 재사용 금지
  • production dist
  • 실제 runtime config/release manifest
  • base path와 hashed chunks
  • trace, video 또는 screenshot evidence

merge gate의 주요 E2E도 가능한 한 built-dist를 사용하고, dev E2E는 개발 편의 명령으로 분리한다.

13.2 3-engine 정책

다음 critical smoke는 Chromium, Firefox, WebKit 모두 실행한다.

  • boot
  • public route navigation
  • protected route/session
  • form submit/error
  • dialog/drawer/menu keyboard
  • theme
  • 320px reflow
  • automated a11y
  • deep link
  • lazy chunk

고비용 exhaustive visual matrix는 pinned Chromium 하나로 실행한다. 엔진 차이 위험이 큰 native dialog, date/input, focus behavior는 WebKit과 Firefox에서도 별도 behavior assertion을 유지한다.

13.3 Mobile과 responsive

최소 viewport:

  • 320x720: 최소 reflow
  • 390x844: 일반 mobile navigation/touch
  • 768x1024: tablet
  • 1280x720: desktop
  • 1440x900: wide layout

검증:

  • horizontal overflow
  • sticky header/sidebar
  • mobile drawer focus
  • touch target
  • long translated text
  • landscape
  • software keyboard로 가려질 수 있는 form action
  • dialog max height와 scroll
  • reduced motion
  • dark theme

device descriptor는 browser/device 특성을 흉내 내지만 실제 기기를 완전히 대체하지 않는다. 제품 release policy가 요구하면 iOS Safari/Android Chrome 수동 또는 device-farm smoke를 추가한다.

13.4 시각 회귀

Playwright toHaveScreenshot()을 사용한다.

필수 baseline:

  • app shell compact/wide
  • 각 page template
  • 모든 design-system primitive group
  • loading/empty/error/401/403/404
  • invalid form
  • open dialog/drawer/menu
  • light/dark
  • pseudo-locale/RTL 대표

결정성 규칙:

  • OS, browser, font, device scale을 CI image로 고정
  • animation/caret 비활성화
  • clock/random/network fixture 고정
  • 개인정보와 동적 ID mask
  • update snapshot은 전용 명령과 reviewer 승인을 요구
  • threshold를 높여 실제 차이를 숨기지 않는다.

Chromatic 같은 cloud visual service는 선택이다. 저장소 내부 Playwright baseline은 외부 서비스 없이도 실행 가능해야 한다.

13.5 실패 증거

  • trace: first retry 또는 최종 실패
  • screenshot: 실패 상태
  • HTML report
  • console/page error
  • network failure summary
  • release/build ID

CI retry를 허용하는 경우 첫 실패 trace도 폐기하지 않는다. retry 후 통과한 test는 flaky signal로 집계한다.

14. Coverage 정책

Coverage 목표는 “99% 완성도”와 같은 표현을 숫자로 대신하기 위한 것이 아니다. 높은 위험의 branch가 실행되었는지 확인하는 보조 gate다.

14.1 도구

Vitest coverage provider를 명시적으로 설치하고 coverage script를 추가한다. Vite/Vitest 조합에서는 V8 provider를 기본 후보로 사용하되 source map 정확성을 fixture로 확인한다.

필수 report:

  • text
  • JSON summary
  • LCOV 또는 Cobertura

14.2 위험별 목표

영역 Line/statement 참고 Branch 우선 목표
registry/codec/schema 95~100% 100%에 가깝게
retry/error/auth/chunk policy 95~100% 모든 결정 분기
HTTP/storage/telemetry adapter 90% 이상 모든 failure class
query/mutation/form controller 90% 이상 상태 전이 전부
design-system primitive 숫자보다 state matrix keyboard/error/open 전부
route/page composition 80% 이상 참고 critical route 전부
scripts/release checks 90% 이상 positive/negative fixture

전체 global threshold 하나만 두면 쉬운 파일로 위험한 파일의 누락을 가릴 수 있다. glob 또는 별도 package/명령으로 high-risk module threshold를 강화한다.

14.3 Diff coverage

신규·변경 코드의 executable line과 branch는 기본적으로 테스트되어야 한다. 예외는 다음을 기록한다.

  • 실행 불가능한 defensive line
  • browser engine 전용 branch
  • external evidence만 가능한 branch
  • owner
  • 사유
  • 만료일 또는 제거 조건

14.4 제외

다음만 명시적으로 제외한다.

  • generated type
  • build output
  • story metadata 자체
  • 순수 type-only file
  • schema에서 생성된 code

bootstrap, facade, error fallback을 “테스트가 어렵다”는 이유로 제외하지 않는다.

15. Fixture와 테스트 디렉터리

현재 중앙 test taxonomy를 유지하면서 책임을 명확히 한다.

tests/
  runtime-schema/
  unit/
  contract/
  component/
  integration/
    bootstrap/
    query/
    forms/
    features/
  e2e/
    smoke/
    routes/
    forms/
    accessibility/
  visual/
  mocks/
    contracts/
    handlers/
    scenarios/
  factories/
  fixtures/
    architecture/
    security/
    typecheck/
    contracts/
  support/
    render/
    query/
    router/
    clock/
  setup.ts

Story는 component와 가까이 둔다.

src/adapters/inbound/react/design-system/primitives/button/
  button.tsx
  button.css
  button.stories.tsx

15.1 역할 구분

  • factory: 유효한 typed object를 기본값과 override로 생성
  • fixture: 변경하지 않는 positive/negative 샘플
  • handler: operation request를 intercept
  • scenario: 여러 handler와 상태를 사용자 흐름으로 조합
  • support: render wrapper, deterministic clock, test query client

factory가 production schema를 우회하지 않게 contract test에서 생성 결과를 validate한다.

15.2 파일 이름

  • unit/component/integration: *.test.ts, *.test.tsx
  • Playwright: *.spec.ts
  • story: *.stories.tsx
  • negative fixture는 기대 실패 이유를 directory/name으로 표현

utils.ts, helpers.ts, fixture.ts 같은 의미 없는 단일 파일에 모든 기능을 모으지 않는다.

16. CI Gate 설계

기존 gate taxonomy에 다음 검증을 명시적으로 포함한다.

16.1 Merge gate

  • frozen install
  • production typecheck
  • test/e2e typecheck
  • lint
  • architecture
  • runtime schema
  • unit
  • contract
  • component
  • integration
  • coverage
  • Storybook build
  • Storybook interaction/a11y
  • critical 3-engine E2E
  • automated route a11y
  • signed manual accessibility evidence for every registered route in scope
  • design-system visual baseline
  • production build
  • sample removal
  • security

16.2 Release gate

  • bundle budget
  • built-dist 3-engine smoke
  • base path/deep-link
  • release/config coherence
  • chunk mismatch recovery
  • hosting header
  • lab performance
  • release visual smoke

16.3 Production/field gate

  • rollback/runbook drill
  • provider smoke
  • approved production Web Vitals evidence

외부 배포 주소나 28일 field evidence가 없는 skeleton 개발 단계에서는 해당 gate가 실제 프로젝트가 채울 계약으로 남는다. 이를 fake data로 통과시키지 않는다. 현재 FE-GATE-009의 signed manual accessibility review는 merge gate다. 이 문서는 이를 production/field gate로 이동시키지 않는다.

16.4 병렬화와 shard

  • unit/contract/component는 안정된 shard key를 사용한다.
  • Playwright는 project와 shard를 함께 증거 이름에 포함한다.
  • shard 하나가 실패해도 나머지 증거를 업로드한다.
  • 최종 gate는 모든 shard의 통과를 AND로 계산한다.
  • test order 의존성을 찾기 위해 정기적으로 순서를 섞을 수 있으나 random seed를 증거에 기록한다.

16.5 Flaky policy

  • merge를 통과시키기 위한 임의 test.skip 금지
  • quarantine에는 owner, defect ID, 영향 route, 만료일 필요
  • retry 후 pass도 성공으로만 집계하지 않고 flaky report에 기록
  • 동일 test가 반복되면 clock/network/focus/shared-state 원인을 제거
  • CI machine 성능에 맞추기 위해 assertion을 제거하거나 timeout만 계속 늘리지 않는다.

17. 새 Feature 테스트 Recipe

도메인 feature를 추가할 때 다음 순서를 따른다.

각 단계는 feature capability에 해당할 때 적용한다. 적용되지 않는 query, mutation, form, visual, native-browser 항목은 조용히 생략하지 않고 테스트 계획에 N/A와 이유를 기록한다. 예를 들어 read-only static route에 422, optimistic rollback, dirty form test를 만들지 않는다.

Step 1. 위험과 계약을 열거한다

  • 사용자가 완료하려는 action은 무엇인가?
  • 데이터 손실, 중복 mutation, 권한 노출 위험은 무엇인가?
  • URL, API operation, query key, telemetry event는 무엇인가?
  • loading/empty/stale/error/conflict/access 상태는 무엇인가?
  • keyboard, mobile, locale 요구는 무엇인가?

위험마다 가장 가까운 테스트 계층을 지정한다.

Step 2. Type과 schema를 먼저 닫는다

  • route params/search
  • command/query
  • API request/response
  • domain/application result
  • view model
  • form value
  • query key

positive와 negative type/schema fixture를 추가한다.

Step 3. MSW scenario를 등록한다

feature operation에 필요한 scenario ID를 catalog에 추가한다.

최소:

  • success
  • empty 또는 not-found
  • slow
  • auth/forbidden
  • schema mismatch
  • retryable/terminal failure
  • mutation이면 409/422

기존 공통 scenario로 충분하면 새 이름을 만들지 않는다.

Step 4. Pure policy와 mapper를 unit test한다

  • normalization
  • mapping
  • policy
  • error classification
  • query key
  • optimistic update를 사용하는 mutation의 patch/rollback

DOM이나 MSW를 불필요하게 사용하지 않는다.

Step 5. Query/mutation integration을 작성한다

  • 실제 local query adapter
  • 새 QueryClient
  • 실제 application use case
  • MSW
  • query cancellation/retry, mutation invalidation, 적용되는 경우 optimistic rollback

HTTP client를 vi.fn() 결과로 대체해 boundary contract를 건너뛰지 않는다.

Step 6. Form이 있는 경우 form/controller를 작성한다

  • field semantics
  • client validation
  • 422 mapping
  • submit pending
  • conflict
  • dirty/reset

Step 7. Page component를 검증한다

  • page template
  • initial/loading/empty/success/error
  • actions
  • route params/search
  • heading focus
  • access state

role과 accessible name으로 상호작용한다.

Step 8. Story를 추가한다

  • 모든 주요 state
  • dark
  • compact
  • long/pseudo locale
  • interaction
  • a11y

Step 9. E2E critical flow를 추가한다

  • direct route
  • keyboard flow
  • success
  • 사용자 복구 가능한 failure
  • mobile
  • 3-engine이 필요한 native/focus behavior

implementation detail가 아니라 사용자 결과를 assertion한다.

Step 10. Visual baseline을 추가한다

새 layout, template variant, 상태 표면이 생긴 경우만 baseline을 추가한다. 기존 primitive를 단순히 조합한 모든 feature page를 무조건 screenshot으로 고정하지 않는다.

Step 11. Evidence와 문서를 갱신한다

  • test taxonomy
  • route/accessibility scope
  • CI gate evidence
  • feature recipe
  • 운영 failure/runbook 연결

Step 12. 전체 gate를 실행한다

corepack pnpm check:types
corepack pnpm lint
corepack pnpm check:architecture
corepack pnpm test:runtime-schema
corepack pnpm test:unit
corepack pnpm test:component
corepack pnpm test:integration
corepack pnpm test:e2e
corepack pnpm test:a11y
corepack pnpm build
corepack pnpm check:bundle

TypeScript test, Storybook, coverage, visual, built-dist 명령이 도입되면 위 목록과 CI registry에 추가한다.

18. Feature Definition of Done

아래 체크는 feature에 존재하는 capability에 적용한다. N/A에는 이유와 해당 위험을 대신 검증하는 계층을 기록한다. 공통 type, architecture, bootstrap, 접근성, security gate는 임의로 N/A 처리할 수 없다.

Type과 계약

  • production source와 test source typecheck가 통과한다.
  • route/API/query/form type이 closed contract다.
  • positive와 필요한 negative schema fixture가 있다.
  • registry와 runtime mapping이 exhaustive하다.

Unit과 integration

  • pure policy와 mapper의 모든 중요 branch를 검증했다.
  • MSW shared scenario를 사용한다.
  • unhandled request가 test를 실패시킨다.
  • query가 있으면 cancellation, retry, stale 상태를 검증했다.
  • mutation이 있으면 invalidation/conflict를, optimistic update를 쓰면 rollback을 검증했다.
  • auth recovery가 bounded임을 검증했다.

Form과 UI

  • form이 있으면 field label/description/error가 연결된다.
  • form이 있으면 error summary와 first-invalid focus가 동작한다.
  • mutation form이 있으면 pending과 double submit을 검증했다.
  • loading/empty/success/error/access 상태가 있다.
  • keyboard와 focus test가 있다.
  • component/story a11y가 통과한다.

Router와 bootstrap

  • deep link와 browser navigation이 동작한다.
  • params/search invalid 입력이 안전하게 처리된다.
  • route error를 검증하고 lazy chunk가 있는 경우 recovery를 검증했다.
  • 실제 bootstrap/provider composition에서 feature가 동작한다.
  • built-dist smoke가 통과한다.

Browser와 visual

  • native/focus/browser 차이가 있는 critical flow가 지정된 3개 엔진에서 통과하고, 나머지는 browser matrix 정책을 따른다.
  • 320px와 대표 mobile viewport에서 overflow가 없다.
  • 필요한 light/dark visual baseline이 있다.
  • pseudo-locale/긴 문구가 잘리지 않는다.
  • failure trace와 report가 CI evidence로 남는다.

Coverage와 운영

  • high-risk module branch 목표를 충족한다.
  • 신규 코드의 미검증 branch에 승인 없는 예외가 없다.
  • bundle/performance budget을 통과한다.
  • telemetry/error output에 민감 정보가 없다.
  • 관련 gate와 evidence registry가 갱신되었다.

19. 금지 패턴

  • production bootstrap을 복사한 test-only shell만 검증
  • query cache singleton을 test 사이에 공유
  • fetch나 HTTP client 전체를 vi.fn()으로 대체하고 MSW integration 생략
  • 성공 응답만 제공하는 feature mock
  • test마다 서로 다른 401/422/500 body를 임의 작성
  • test에서 실제 sleep 사용
  • timeout을 늘려 race condition 숨김
  • implementation class와 DOM nesting만 assertion
  • 대형 DOM snapshot으로 behavior test 대체
  • failure screenshot을 visual regression이라고 부름
  • dev server E2E만으로 release build 완료 선언
  • Chromium만 통과하고 native focus/browser 호환 완료 선언
  • axe 결과만으로 접근성 완료 선언
  • global coverage 숫자로 고위험 branch 누락 은폐
  • type error를 any, @ts-ignore, 이중 cast로 숨김
  • production bundle에 MSW worker, mock selector, fake credential 포함
  • flaky test를 owner/만료일 없이 skip
  • CI gate에 continue-on-error

20. 단계별 도입 순서

  1. tsconfig.test.json과 test typecheck gate를 추가한다.
  2. production bootstrap을 주입 가능한 함수로 추출하고 integration test를 만든다.
  3. MSW handlers/scenario/factory를 중앙 catalog로 이동한다.
  4. query/mutation presentation adapter와 integration harness를 만든다.
  5. form foundation과 form/controller test matrix를 만든다.
  6. route registry/runtime map contract와 built-dist artifact 검증을 유지하고, release server를 사용하는 built-dist E2E까지 확장한다.
  7. Storybook build, interaction, a11y gate를 추가한다.
  8. pinned Chromium visual baseline을 추가한다.
  9. critical flow의 3-engine release profile을 분리한다.
  10. V8 coverage와 high-risk/diff policy를 차단 gate로 추가한다.
  11. feature recipe와 Definition of Done를 pull request template에 연결한다.

이 순서의 완료 기준은 테스트 도구를 모두 설치하는 것이 아니다. 새 도메인 기능이 추가되었을 때 개발자가 성공·실패·권한·복구·접근성·브라우저·release 위험을 어디서 어떻게 검증할지 추가 설계 없이 결정할 수 있어야 한다.