Files
tech-log-frontend/docs/testing/frontend-platform-testing-strategy.md
T

1351 lines
45 KiB
Markdown

# 프론트엔드 플랫폼 테스트 전략
이 문서는 도메인 기능을 추가하기 전과 추가한 후에 프론트엔드 플랫폼의 위험을
어느 테스트 계층에서 차단할지 정의한다. 목표는 테스트 개수를 늘리는 것이 아니라,
실패 원인과 가장 가까운 계층에서 빠르고 결정적으로 결함을 발견하고 실제
브라우저·실제 production build에서만 드러나는 위험을 별도 게이트로 닫는 것이다.
이 문서는 다음 공식 문서를 기술 기준으로 사용한다.
- [Vitest 공식 가이드](https://vitest.dev/guide/)
- [Mock Service Worker 공식 문서](https://mswjs.io/docs/)
- [Playwright 공식 문서](https://playwright.dev/docs/intro)
- [Playwright 시각 비교 문서](https://playwright.dev/docs/test-snapshots)
- [Storybook UI 테스트 문서](https://storybook.js.org/docs/writing-tests)
## 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:coverage`
- `test:e2e`
- `test:e2e:dev`
- `build:storybook`
- `test:storybook`
- `test:visual`
- `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 RP-10에서 닫힌 공백과 남은 외부 범위
#### 테스트 TypeScript typecheck 기반
`check:types`는 app, Node scripts/config, tests project를 순서대로 검사한다.
테스트 project는 TS/TSX callback, mock과 config type을 검사하되 실패를
의도한 `tests/fixtures`는 별도 TypeScript negative command가 소유한다.
Vitest의 변환 성공을 TypeScript typecheck의 대체물로 취급하지 않는다.
#### 실제 bootstrap integration
runtime config와 release manifest를 검증한 composition root에서 실제 provider
순서와 application input을 연결하는 component/integration test를 제공한다.
production Playwright profile은 source fixture가 아니라 `build` + `preview`
실제 entry와 hashed route chunk를 사용한다.
#### TanStack Query의 React integration test 기반
`tests/component/application-query.test.tsx`는 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과 route 위험
form component/reference feature test가 error summary, 첫 오류 focus, Zod
transform, 422 allowlist, double submit, dirty navigation, optimistic rollback과
conflict를 검증한다. route registry/runtime 양방향 참조, codec, lazy module,
location reset, scroll restoration, blocker와 bounded chunk recovery도
unit/component/built artifact 검증에 연결됐다.
#### Storybook과 시각 회귀
public design-system primitive를 실제 platform provider로 렌더하는 dev-only
Storybook, interaction/axe test와 production build를 대상으로 한
`toHaveScreenshot()` baseline 4종을 제공한다. cloud review, 다중 OS font
rasterization과 실제 device farm은 프로젝트가 요구할 때 연결한다.
#### Shared MSW와 결정성
operation별 success/empty/slow/network/timeout/content/envelope/schema/auth/
403/404/409/422/429/retry/terminal을 포함한 19개 scenario catalog와 strict
unhandled-request server를 공유한다. clock/random/scheduler/storage helper와
browser console/page/network failure 정책으로 비결정적 우회를 차단한다.
#### Built-dist와 compact E2E
기본 `test:e2e`는 CI에서 기존 server를 재사용하지 않고 `build` + `preview`
Chromium, Firefox, WebKit과 compact project로 실행한다. 빠른 Vite 개발 profile은
`test:e2e:dev`로 분리한다.
#### 위험 기반 coverage
V8 text/JSON/LCOV를 생성하고 전체 기준과 retry/storage/telemetry/application
composition/compatibility/performance/promotion/chunk/diagnostics/reference HTTP
operation/query-mutation/registry compatibility 19개 high-risk module에 80개
scoped threshold를 적용한다. critical module 누락 또는 threshold 미달 fixture는
merge gate를 실패시킨다.
Repository inventory의 정적 provenance는 runtime 실행 가능 여부가 아니라 현재
Vitest/V8 producer가 file counter를 생성하는 문장의 존재 여부를 나타낸다. 따라서
artifact는 `counterBearingTotal`, `instrumentedCounterBearingTotal`,
`counterlessTotal`, `counterlessModules`를 기록한다. 실제 microfixture에서
`import type {}`, `import {}`, bare side-effect import, value import, named/star value
re-export만 있는 모듈은 모두 exact all-zero row였고, 선언/초기화 또는 직접 실행문이
있는 모듈은 counter-bearing이었다. Counterless import/re-export도 module evaluation
과 side effect를 유발할 수 있으므로 이를 non-executable로 해석하지 않는다. Gate는
static counterless 집합과 exact all-zero row 집합의 일치를 요구하고 critical/high-risk
policy module이 counterless이면 실패시킨다.
Coverage policy 입력은 schema version 2를 유지한다. 반면 위 counter-bearing 필드를
직렬화하는 risk-coverage 결과 artifact는 schema version 3이다. 두 version은 서로
독립적인 계약이며, 실제 CLI contract test가 artifact의 새 필드와 legacy 필드 부재를
검증한다.
남은 범위는 실제 device/browser farm, cloud visual approval, 외부 인증·telemetry
provider와 production field data다. 이 증거가 없을 때 저장소 내부 test를
`PRODUCTION_READY`의 대체물로 사용하지 않는다.
## 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 목표 구성
```text
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`
권장 명령:
```bash
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을 증명하지 않는다.
```text
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 예:
```ts
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 순서를 사실대로 검증한다.
```text
QueryClientProvider
-> BrowserRouter
-> ThemeProvider
-> SessionProvider
-> AppShell
```
각 provider를 mock component로 대체해 문자열 순서만 검사하지 않는다. 실제
Context/hook을 소비하는 작은 probe route를 렌더링해 provider가 사용 가능한지
확인한다.
application input facade를 도입한 목표 순서는 다음과 같다.
```text
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 뒤에 연결한 뒤 다음 계층을
제공한다.
```text
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로 오분류되지 않는다.
- success와 retry recovery는 terminal failure telemetry를 만들지 않고,
exhausted retry는 logical execution당 한 번만 발행한다.
- 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
```text
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.tsx`
`tests/e2e/reference-form.spec.ts`에 있다. 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.tsx`
`tests/e2e/design-system-interactions.spec.ts`다. Story interaction/visual
baseline은 VD-08/RP-10에서 추가하며 현재 runtime gallery를 isolated workshop
완료 증거로 사용하지 않는다.
RP-08의 국제화 실행 경로는 `check:i18n`, `check:i18n:fixture`,
`check:types:fixture:i18n-key`, `check:types:fixture:i18n-params`,
`tests/unit/i18n-contract.test.ts`, `tests/component/locale-platform.test.tsx`
`tests/e2e/i18n.spec.ts`다.
- unit: locale catalog/placeholder parity, safe fallback/alias, pseudo expansion,
direction과 UTC date/number/relative/list/plural/select
- component: document `lang/dir`, RTL tab arrow semantics, direction-aware
pagination과 modal Drawer
- E2E: 320px pseudo reflow와 compact RTL shell/Drawer/focus restore
- negative: common UI locale literal, backend message JSX render, raw translated
HTML, unknown key와 잘못된 interpolation parameter
`ar-EG` catalog가 영어 smoke copy를 재사용하는 것은 RTL behavior 검증용이며
번역 품질 PASS가 아니다. 실제 locale catalog에는 언어 담당 승인 evidence가
추가되어야 한다.
### 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](https://mswjs.io/docs/)는 request client를 mock하지 않고 HTTP 경계에서
요청을 가로챈다. Node integration과 browser workshop이 동일한 operation contract
및 scenario vocabulary를 사용하도록 중앙 catalog를 만든다.
설치된 feature의 catalog와 handler 증적은
`config/testing/test-evidence.json`의 owner별 contribution으로 등록한다. 따라서
feature 제거 시 해당 contribution도 함께 제거할 수 있고 공통 evidence checker는
특정 sample 경로를 하드코딩하지 않는다.
### 11.1 목표 구조
```text
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 환경별 사용
```text
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 검증
```text
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를 유지하면서 책임을 명확히 한다.
```text
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와 가까이 둔다.
```text
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를 실행한다
```bash
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 명령은
`config/ci/gates.json`의 blocking step과 JUnit/HTML/trace/fixture evidence에
연결되어 있다.
## 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을 통과한다.
- [x] diagnostics/telemetry output의 allowlist, value policy와 negative fixture가
raw URL/query/body/storage value/error object를 거절한다.
- [ ] 관련 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`
### 선택형 adapter recipe gate
선택형 capability example은 `tests/recipes`에서 contract/fake/unavailable을
실행하지만 production entry에는 포함하지 않는다. `check:optional-recipes`
12개 catalog 완전성, unselected dependency, production source import와 built
bundle sentinel 부재를 검사한다. negative fixture는 lifecycle cleanup 누락,
vendor direct import, credential storage/URL/telemetry 경로와 workflow store의
server-state 복제를 거절한다. `test:optional-recipe-removal`은 recipe 전체를
제거한 사본에서 base typecheck/test/build를 다시 실행한다.
## 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
위험을 어디서 어떻게 검증할지 추가 설계 없이 결정할 수 있어야 한다.