컴포넌트들이 모여 어떠한 하나의 기능을 수행할 때, 서버 통신이 개입되는 경우가 많다.
이 때, 만약 백엔드 단에서 api가 덜 만들어졌거나 원하는 결과를 넘겨주지 않는다면 먼저 프론트 개발을 들어가기도 어렵고, 완성되지 않은 api를 요구하는 기능의 테스트를 건너뛸 수 밖에 없다.
결국 서버 통신이 요구되는 어떠한 기능이 잘 돌아가는지 보장할 수도 없고, 추후에 추가하더라도 이미 다른 코드들이 많이 짜여져있는 상황인데 이 기능의 예상치 못한 버그로 인해 많은 코드를 고쳐야할 수도 있다.
이러한 상황을 방지하기 위해 nock과 같은 라이브러리도 있지만, 나는 MSW를 도입하기로 하였다.
MSW란?
MSW는 API 요청을 가로채서 사전에 설정해둔 목업 데이터를 넘겨주도록 설정해 주는 도구이다.
nock과 같은 도구도 마찬가지이지만 많은 곳에서 MSW 사용을 선호하고 있다.
가장 큰 차이점은 nock은 노드 환경에서만 돌아가지만 MSW는 브라우저에서도 실제 http 요청을 가로챌 수 있다는 점이다.
실제 http 요청을 가로챌 수 있는 이유는 MSW는 서비스워커를 사용하기 때문이다.
서비스워커(service worker)란?
서비스 워커는 웹 페이지와 별도로 브라우저가 백그라운드에서 실행되는 스크립트로 응용 프로그램, 브라우저, 그리고 네트워크 사이의 프록시 서버 역할을 한다고 정의되어 있다.
* 프록시 서버: 클라이언트가 자신을 거쳐 다른 네트워크에 접속할 수 있도록 중간에서 대리해주는 서버
즉, MSW는 이런 서비스 워커를 사용하기 때문에 실제 네트워크 요청이 발생하기 전에 가로채어 모킹된 응답을 제공할 수 있는 것이다.
이 덕분에 테스트 환경을 실행만 시킨 후 개발 서버를 구동하면, 실제 서버 요청이 가는 대신 내가 만든 모킹된 응답으로 UI를 직접 보며 모의 테스트를 진행할 수 있게 된다.
MSW 세팅(v2)
이제 MSW를 Next14 프로젝트에 세팅해보자!
yarn add -D msw
위의 명령어로 msw를 설치해주고,
npx msw init ./public --save
서비스 워커를 등록하기 위해서는 스크립트를 호스팅하고 제공하여야하기 때문에 위 명령어를 통해 public 폴더 위치에 서비스 워커를 등록해준다.
위 명령어를 수행하면 public 폴더 내에 mockServiceWorker 파일이 만들어져 있는 것을 볼 수 있다.
기본적인 세팅인 위가 다지만, 실제로는 처리해야하는 수많은 에러들이 남아있다 ...ㅋㅋ
회원가입 기능을 테스트해보며 추가적인 에러들을 고쳐나가보기로 했다.
Test
조건에 맞는 input들이 입력되고 가입하기 버튼을 누르면 로딩 UI가 뜨고, 통신이 성공하면(status:200) 가입 성공 페이지로 이동한다.
위의 기능을 테스트해야한다.
자세한 로직 순서는 다음과 같다.
아이디, 비밀번호, 닉네임 input들을 올바르게 채운다 -> 가입하기 버튼을 누른다 -> 클라이언트 사이드에서 '/api/auth/register' url로 설정해준 Route Handler가 호출된다. -> 로딩 UI가 뜬다 -> status:200 으로 성공 응답이 오면 '/register/success' 라우트로 이동한다.
위 로직을 구현한 코드는 다음과 같다.
const handleSubmit = async (e: LoginFormState | RegisterFormState) => {
if ("nickname" in e) {
changeFetching(true);
const res = await fetch("/api/auth/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(registerFormData),
});
if (res.status === 200) {
changeFetching(false);
router.push("/register/success");
} else {
alert("회원가입 과정 중 오류가 발생했습니다");
}
}
};
이제 테스트를 시작해보자.
그런데 Jest에 MSW를 적용해보려고 하자 마자 에러를 마주칠 것이다.
TextEncoder is not defined
아니 벌써 에러가 뜬다고...?
불행중 다행히 공식문서에서 친절히 polyfill을 추가해야한다고 안내해준다.
공식문서의 안내대로 jest.polyfills.js 파일을 만들고
const { TextDecoder, TextEncoder } = require("node:util");
const { ReadableStream, TransformStream } = require("node:stream/web");
Object.defineProperties(globalThis, {
TextDecoder: { value: TextDecoder },
TextEncoder: { value: TextEncoder },
ReadableStream: { value: ReadableStream },
TransformStream: { value: TransformStream },
});
const { Blob, File } = require("node:buffer");
const { fetch, Headers, FormData, Request, Response } = require("undici");
Object.defineProperties(globalThis, {
fetch: { value: fetch, writable: true },
Blob: { value: Blob },
File: { value: File },
Headers: { value: Headers },
FormData: { value: FormData },
Request: { value: Request },
Response: { value: Response },
});
TextEncoder를 포함하여 다른 내용들의 에러들도 미리 방지해주기 위해 위의 내용을 추가해주자.
그 후 jest.config.ts 파일의 setupFiles 속성에
setupFiles: ["<rootDir>/jest.polyfills.js"],
위와 같이 추가해주면 오류가 사라지게 된다.
또 msw 버전 2에서는 Cannot find module 'msw/node' 에러가 뜨는 이슈가 있다.
이를 해결하기 위해서는 jest.config.ts 파일에
testEnvironmentOptions: {
customExportConditions: [""],
},
위 내용을 추가해주라고 공식문서에 나와있고, 실제로 에러가 사라지는 것을 확인할 수 있다.
그런데 또 남아있는 오류가 있다.
코드 로직을 보면 res.status===200이 만족되면 router.push를 통해 가입 완료 페이지로 이동하는 로직이 있는데 테스트코드로 가입하기 버튼을 누르면 에러가 뜬다.
이를 해결하기 위해 next/navigation 모듈을 mocking 해줘야한다.
나는 이전에 만들어주었던 jest.setup.ts 파일에
export const pushMock = jest.fn();
jest.mock("next/navigation", () => {
return {
__esModule: true,
usePathname: () => ({
pathname: "",
}),
useRouter: () => ({
push: pushMock,
replace: jest.fn(),
prefetch: jest.fn(),
}),
useSearchParams: () => ({
get: () => {},
}),
};
});
위의 내용을 추가해주었다.
그러면 jest 테스트 환경에서 push 동작을 테스트할 때 pushMock을 대신 사용해주면 된다!
이제 api mocking을 위한 handler를 작성해보자.
import { HttpResponse, http } from "msw";
export const handlers = [
http.post("/api/auth/register", async ({ request }) => {
console.log(request);
return HttpResponse.json({
status: 200,
});
}),
];
handlers.ts 파일에 위 코드를 작성해주었다.
handlers.ts 파일들은 테스트가 쌓일수록 많아질 것이라서 잘 정리해놔야 한다.
나는 mocking할 api 경로 그대로 파일들을 정리해주었다(위 파일은 /api/auth/register 경로에).
이제 해당 api로 통신이 오면 위 handler가 통신을 가로챌 수 있게 노드 환경과 브라우저 환경을 세팅해야 한다.
나는 server.ts 파일을 하나 만들어서
import { setupServer } from "msw/node";
import { handlers } from "./api/auth/register/handlers";
export const server = setupServer(...handlers);
노드 환경일 때 handlers를 등록해주었고,
browser.ts 파일도 만들어서
import { setupWorker } from "msw/browser";
import { handlers } from "./api/auth/register/handlers";
export const worker = setupWorker(...handlers);
브라우저 환경일 때도 등록해주었다.
그리고 index.ts 파일에
export async function initMsw() {
if (typeof window === "undefined") {
const { server } = await import("./server");
server.listen();
} else {
const { worker } = await import("./browser");
await worker.start();
}
}
위처럼 작성해 줌으로써 두 환경에 대해 mocking을 수행할 수 있는 실행 로직을 짜 주었다.
"use client";
import { useEffect, useState } from "react";
export const MSWProvider = ({ children }: { children: React.ReactNode }) => {
const [mswReady, setMswReady] = useState(false);
useEffect(() => {
const init = async () => {
const initMsw = await import("../../../../../__mocks__/next/index").then(
(res) => res.initMsw
);
await initMsw();
setMswReady(true);
};
if (!mswReady && process.env.NODE_ENV === "development") {
init();
}
}, [mswReady]);
return <>{children}</>;
};
이제 위 코드 내용의 provider를 하나 만든 뒤 layout에 감싸주기만 하면 개발 서버에서 api mocking할 준비는 끝났다.
yarn test를 돌려주고 개발 서버 콘솔을 보면
위처럼 Mocking이 가능하다는 문구가 뜨게 된다!
개발 서버에서 직접 테스트 말고 터미널에서 yarn test만을 통해 테스트를 할 때도 있다.이를 위해 jest.setup.ts 파일에
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
위 내용을 추가해주면 매 test 마다 노드 환경에서 mocking이 가능하도록 listen, 매 테스트가 끝날때마다 handler를 초기화, 그리고 모든 test가 끝나고 노드 환경에서의 mocking을 끝내주게 된다.
이제 모든 준비가 끝났다.
test("가입하기 버튼을 누르면 로딩 UI가 뜬 후, 통신이 성공하면 가입성공 페이지로 이동한다.", async () => {
const registerButton = screen.getByRole("button", { name: "가입하기" });
expect(registerButton).toBeDisabled();
//when: 비밀번호 형식(영어, 숫자, 특수기호 중 하나라도 포함되지 않는 비밀번호)에 맞지 않는 비밀번호가 입력됨
const idInput = screen.getByLabelText("아이디 (이메일)");
const passwordInput = screen.getByLabelText("비밀번호 (8~16자)");
const nicknameInput = screen.getByLabelText("닉네임");
fireEvent.change(idInput, { target: { value: "brian990614@naver.com" } });
fireEvent.change(passwordInput, { target: { value: "Gusals990!" } });
fireEvent.change(nicknameInput, { target: { value: "brian" } });
expect(registerButton).toBeEnabled();
fireEvent.click(registerButton);
const loadingComponent = screen.getByTestId("loading-ui");
expect(loadingComponent).toBeInTheDocument();
await waitFor(() => {
expect(pushMock).toHaveBeenCalledWith("/register/success");
});
});
registerButton을 fireEvent객체의 click을 통해 클릭하는 부분이 추가되었다.
원래는 클릭을 하면 'api/auth/register' 경로의 Route Handler를 불러오고, 해당 route.ts 파일 내에서 백엔드 서버와 통신을 해야한다.
그런데 yarn test로 테스트를 수행하면 실제 서버에 요청이 가는게 아니라 위 handler에 설정해놓은 같은 경로로 요청이 오니까 mocking을 할 수 있게 된다.
mocking 응답으로 나는 HttpResponse로 status 200을 내려주고 있다.
실제 로직은 status 200을 받기 전에 로딩 UI가 뜨고, status 200을 확인하면 가입 완료 페이지로 이동하는 것이였으므로 이것만 확인해보자.
이를 위해 클릭 즉시 로딩 UI가 뜨는지 getByTestId와 toBeInTheDocument로 확인하고 있다.
이제 mocking된 응답을 받아와야 하는 비동기 통신을 위해 await waitFor를 활용한다.
mocking으로 status 200을 내려주고 있으므로 가입 성공 페이지로 이동해야한다.
위에서 next/navigation 모듈을 mocking 해주었을 때, push 대신 pushMock이라는 모킹 함수를 생성해 주었었다.
이와 toHaveBeenCallWith를 활용하여 목표한 route로 잘 이동되었는지 확인해주었다.
위와 같이 테스트가 잘 돌아가는 것을 확인할 수 있다!!
이제 최소한 가입하기 버튼을 누르면 로딩 UI가 뜬다는 사실 & status 200으로 응답이 온다면 가입 성공 페이지로 무사히 이동된다는 사실은 보장할 수 있게 되었다.
MSW가 service worker를 활용하는 덕분에 위처럼 개발 서버를 키고 직접 UI를 보며 테스트 할 수도 있다.api 완성이 안되었을 때 get 메소드로 가져와야 하는 정보를 mock 데이터로 처리하고 작업할 때 매우 유용하게 사용할 수 있을 것 같다~~
참고
https://www.handongryong.com/post/msw/
MSW로 API Mocking으로 개발환경을 높이고 Next.js RSC환경에서도 MSW 적용하기
MSW로 API Mocking을 하여 프론트엔드 개발 환경을 개선하고 Next.js의 App router의 RSC환경에서 MSW를 적용하는 방법을 알아봅니다.
www.handongryong.com
MSW를 활용하는 Front-End 통합테스트 | 카카오엔터테인먼트 FE 기술블로그
송기연(Kaki) 음악과 별을 좋아하는 개발자입니다.
fe-developers.kakaoent.com
https://oliveyoung.tech/blog/2024-01-23/msw-frontend/
Next.js에서 MSW(Mock Service Worker)로 네트워크 Mocking하기 | 올리브영 테크블로그
네트워크 Mocking을 위해 고민했던 삽질기
oliveyoung.tech
FAQ
Common questions about Mock Service Worker.
mswjs.io
'FrontEnd > Test Code' 카테고리의 다른 글
현업에서의 테스트코드 (0) | 2024.07.17 |
---|---|
Cypress로 E2E 테스트 진행하기 (0) | 2024.07.02 |
Next에서 테스트코드 적용해보기 (Feat: Jest) (0) | 2024.06.24 |
테스트 코드 문법 익히기 (Feat: Jest) (2) | 2024.06.21 |
테스트 코드 이론 (0) | 2024.06.19 |