Jest + RTL에서 MSW를 추가하려 했더니, 호환이 잘 안돼서 의존성을 이것 저것 추가를 해도 결국 해결하지 못해 Vitest로 마이그레이션하기로 했다. 그리고 이왕 이렇게 된 것 Webpack말고 Vite도 사용해보고 싶어서 Vite로 마이그레이션하기로 했다. 또 아직 테스트 코드를 많이 짜보지 않아서 심적으로도 큰 벽은 없었다.
npm create vite@latest
먼저 vite 프로젝트 디렉토리를 생성해준다
npm install
필요한 패키지를 설치해준다.
npm install -D vitest jsdom msw @testing-library/jest-dom @testing-library/react @testing-library/user-event @vitest/coverage-v8
/// <reference types="vitest" />
/// <reference types="vite/client" />
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: "jsdom",
setupFiles: "./src/test/setup.ts",
},
});
맨 위처럼 Triple-Slash Directive로 vitest, vite/client의 타입 선언 파일을 참조하도록 하자. 이거 안하면 아래 defineConfig에서 test 설정할 때 타입 오류가 난다. 그리고 defindeConfig에 test도 설정해주자.
여기서 globals, environment, setupFiles는 다음과 같은 역할을 한다.
1. globals: Vitest가 describe, it, expect
와 같은 함수들을 명시적으로 import하지 않고도 사용할 수 있게 해준다. 단, 타입스크립트를 쓰는 경우 에러가 날 수 있기 때문에, 테스트 파일에서도 import를 해주자.
2. environment: Vitest가 테스트를 실행할 때 사용할 환경을 지정한다.
3. setupFiles: 테스트 실행 전에 실행될 파일을 지정한다. 예를 들어, Jest DOM의 matcher를 import 하거나, MSW의 모킹 서버를 설정하는 등의 작업을 할 수 있다.
// src/test/setup.ts
import "@testing-library/jest-dom";
setup.ts 파일을 생성해서 jest-dom
을 테스트 실행 전에 import하게 한다.
Vitest가 Jest의 API와 호환되는데 jest-dom
은 jest DOM 관련 매처를 사용할 수 있게 해준다.
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"test": "vitest"
},
package.json에 test script를 넣는다.
export default function MyComponent() {
return <div>Hello, world!</div>;
}
import { render, screen } from "@testing-library/react";
import { describe, expect, test } from "vitest";
import MyComponent from "./MyComponent";
describe("MyComponent", () => {
test("render Hello, world!", () => {
render(<MyComponent />);
const Welcome = screen.getByText("Hello, world!");
expect(Welcome).toBeInTheDocument();
});
});
맨처음에는 서비스 워커를 이용해 모킹하는 Browser 세팅만 했더니 브라우저에서 모킹 테스트는 잘 됐지만, VSC에서 Vitest와 RTL과 결합한 테스트는 안되는 현상이 발생했다. 그래서 Node 세팅을 하니 이번에는 VSC에서는 잘 되지만 브라우저에서는 에러가 뜨는 현상이 발생했다. 고민을 해보니 그냥 둘다 세팅하면 되지 않나? 해서 둘 다 세팅을 했다.
// src/test/mocks/handler.ts
import { http, HttpResponse } from "msw";
export interface UserResponse {
id: number;
username: string;
}
const user: UserResponse = {
id: 1,
username: "giho",
};
export const handlers = [
http.get("/api/user", () => {
return HttpResponse.json(user);
}),
];
모킹을 위한 API 세팅이다.
import { setupServer } from "msw/node";
import { handlers } from "./handler";
export const server = setupServer(...handlers);
주의할 점은 setupServer를 msw/node에서 import해야한다.
import "@testing-library/jest-dom";
import { beforeAll, afterAll, afterEach } from "vitest";
import { server } from "./mocks/server";
beforeAll(() => server.listen());
afterAll(() => server.close());
afterEach(() => server.resetHandlers());
Node의 경우 setup.ts에서 VSC 테스트를 시작하기 전에 서버를 실행하게 끔 한다.
npx msw init <PUBLIC_DIR> --save
Borwser에서는 서비스 워커를 사용하므로 그에 필요한 스크립트 파일을 만들어줘야한다. Vite의 경우 ./public에 설치하면 되므로
npx msw init ./public --save
이렇게 하면 된다.
import { setupWorker } from "msw/browser";
import { handlers } from "./handler";
export const worker = setupWorker(...handlers);
마찬가지로 setupWorker를 msw/browser에서 import하는 것을 주의한다.
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
async function enableMocking() {
if (process.env.NODE_ENV !== "development") {
return;
}
const { worker } = await import("./test/mocks/browser.ts");
// `worker.start()` returns a Promise that resolves
// once the Service Worker is up and ready to intercept requests.
return worker.start();
}
enableMocking().then(() => {
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
);
});
Browser의 경우 main.tsx에서 실행되게 세팅해야한다.
Browser에서는 딱히 테스트 코드를 짤 필요없이 컴포넌트 코드와 handler 코드만 있으면 된다.
// src/app.tsx
import { useEffect, useState } from "react";
import "./App.css";
import { UserResponse } from "./test/mocks/handler";
function App() {
const [user, setUser] = useState<UserResponse | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const fetchUser = async () => {
setIsLoading(true);
try {
const response = await fetch("/api/user");
if (!response.ok) throw new Error("에러 발생");
const data = await response.json();
setUser(data);
} catch (error) {
if (error instanceof Error) setError(error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchUser();
}, []);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error...</div>;
return (
<>
{user && (
<div>
<h1>User</h1>
<p>Username: {user.username}</p>
</div>
)}
</>
);
}
export default App;
간단히 유저 네임을 가져와서 렌더링을 하는 컴포넌트다. fetch의 url에 handler로 만들어 놓은 가짜 API 주소를 넣어준다.
잘 작동한다. 관리자 도구를 보면 [MSW] Mocking enabled.
이렇게 뜨는게 MSW가 잘 작동한다는 표시이다.
Node 테스트 코드의 경우 test의 콜백 함수 맨 앞부분에 API를 넣어주면 된다.
import { describe, expect, test } from "vitest";
import { render, screen } from "@testing-library/react";
import App from "./App";
import { server } from "./test/mocks/server";
import { http, HttpResponse } from "msw";
describe("테스트", () => {
test("vitest 테스트", () => {
expect(true).toBeTruthy();
});
test("유저 데이터를 불러와서 렌더링한다.", async () => {
render(<App />);
expect(screen.getByText("Loading...")).toBeInTheDocument();
expect(await screen.findByText("User")).toBeInTheDocument();
});
test("에러가 발생하면 에러 메시지를 렌더링한다.", async () => {
server.use(
http.get("/api/user", () => {
return HttpResponse.json(
{ message: "에러 발생" },
{ status: 401 }
);
})
);
render(<App />);
expect(screen.getByText("Loading...")).toBeInTheDocument();
expect(await screen.findByText("Error...")).toBeInTheDocument();
});
});
.use()
메서드로 기존 핸들러를 오버라이딩해 API 호출이 실패할 경우를 만들어준다.
테스트가 잘 작동한다.
이제 세팅은 지겨우니까 제발 테스트 코드 좀 짜보자!