Skip to Content
E2eMSW Mock 작성

MSW Mock 작성

E2E 테스트에서 모든 API 호출은 mocks/handlers.ts에서 MSW로 mocking합니다. 실제 DB를 오염시키지 않고 다양한 시나리오를 재현할 수 있습니다.

handlers.ts 작성법

핸들러 하나가 여러 시나리오를 담당합니다. request body에 따라 응답을 분기하면 하나의 핸들러로 정상/에러 케이스를 모두 처리할 수 있습니다.

// mocks/handlers.ts import {http, HttpResponse} from 'msw'; import {createMSWResolver, e2eRequestBodyDeepEqual} from './utils'; export const e2eMockHandlers = [ http.post( howmuchhomeApiOrigin + '/api/' + getAPIPath(AuthAPI.createAuthIdentityVerification), createMSWResolver(async ({request}) => { const body = (await request.json()) as CreateAuthIdentityVerificationRequest; if (e2eRequestBodyDeepEqual(body, { name: '이미 인증한 유저', ... })) { return HttpResponse.json( { errorCode: 'IDENTITY_VERIFICATION_DUPLICATION' }, { status: 400 }, ); } return HttpResponse.json({ authIdentityVerification: { id: 'abc', ... } }); }), ), ];

핸들러에 정의된 분기가 곧 테스트에서 재현 가능한 시나리오의 전체 목록입니다. 테스트 코드를 작성하기 전에 먼저 필요한 분기를 핸들러에 설계하세요.

인증 상태 분기

Authorization 헤더의 토큰 값으로 로그인 여부나 사용자 상태를 분기합니다.

http.get( howmuchhomeApiOrigin + '/api/' + getAPIPath(UserAPI.getMe), createMSWResolver(info => { const token = mswGetAccesstoken(info.request); switch (token) { case 'token': return HttpResponse.json({ me: { id: 1, name: '이름', ... } }); default: return HttpResponse.json({ me: null }); } }), ),

유틸리티

createMSWResolver

모든 핸들러는 반드시 이 함수로 감싸야 합니다.

http.post(url, createMSWResolver(async ({request}) => { ... }))

*.vercel.app / localhost 이외의 도메인에서는 mocking 없이 실제 API를 통과(passthrough)시킵니다. 감싸지 않으면 실서비스 도메인에서도 API가 mocking되어 실제 데이터가 오염될 수 있습니다.

e2eRequestBodyDeepEqual

request body가 특정 형태와 정확히 일치하는지 확인합니다.

if (e2eRequestBodyDeepEqual(body, { name: '이름', carrier: 'SKT', ... })) { return HttpResponse.json({ ... }); }

⚠️ 중요: MSW가 수신하는 request body는 항상 snake_case입니다.

fetcherFactory는 서버로 요청을 보내기 전에 body의 키를 camelCase → snake_case로 변환합니다. 즉, MSW가 인터셉트하는 시점에는 이미 verifyTokenverify_token, registrationKeyregistration_key로 변환된 상태입니다.

따라서 **핸들러에서 body.verifyToken 등 camelCase로 직접 접근하면 항상 undefined**가 됩니다. 반드시 e2eRequestBodyDeepEqual을 사용해야 합니다. 이 함수는 두 객체를 모두 snake_case로 변환한 뒤 비교하므로, 핸들러 작성 시 camelCase 타입 그대로 비교 대상을 전달하면 됩니다.

// ❌ 잘못된 예시 — 항상 false (body의 실제 키는 verify_token) if (body.verifyToken === '123456') { ... } // ✅ 올바른 예시 — e2eRequestBodyDeepEqual이 내부에서 snake_case 변환 후 비교 if (e2eRequestBodyDeepEqual(body, {verifyToken: '123456'})) { ... }

⚠️ path param도 body에 포함됩니다.

API 클래스의 this.fetch(props)는 props 전체를 body로 전송하므로, URL path에 사용되는 필드(예: verificationId)도 실제 request body에 포함됩니다. e2eRequestBodyDeepEqualdeepEqual이므로 비교 대상에 body의 모든 필드를 포함해야 합니다.

// ❌ 잘못된 예시 — body에는 verification_id도 포함되어 있어 deepEqual 실패 if (e2eRequestBodyDeepEqual(body, {verifyToken: '123456'})) { ... } // ✅ 올바른 예시 — path param 포함 const body = (await request.json()) as ConfirmIdentityVerificationRequest; if (e2eRequestBodyDeepEqual(body, { verificationId: 'delegate-iv-id', verifyToken: '123456', })) { ... }

waitForHydration

페이지 이동 후 MSW Service Worker 활성화 + React hydration 완료를 대기합니다. MSWProviderSuspense + use(mswReadyPromise)로 children 렌더를 차단하므로, SW controller가 활성화된 시점에 hydration도 완료된 상태입니다.

import {waitForHydration} from '../mocks/utils'; await page.goto(url); await waitForHydration(page); // navigator.serviceWorker.controller 활성화 대기

location.replace, page.goto, page.evaluatefull page navigation 후에 반드시 호출해야 합니다. SPA navigation(Link 클릭, router.push 등)에서는 불필요합니다.

mswGetAccesstoken

Authorization 헤더에서 Bearer 토큰을 추출합니다.

const token = mswGetAccesstoken(request); // → 'token' | undefined

mswGetRequestBody

request body를 camelCase로 변환하여 반환합니다. snake_case로 오는 응답을 앱 코드 컨벤션에 맞게 읽을 때 사용합니다.

const body = await mswGetRequestBody<MyRequestType>(request);
Last updated on