Vite에서 Vitest + RTL + MSW 테스트 세팅

고기호·2024년 9월 3일
1

테스트

목록 보기
3/3

Webpack과 Jest에서 마이그레이션

이유


Jest + RTL에서 MSW를 추가하려 했더니, 호환이 잘 안돼서 의존성을 이것 저것 추가를 해도 결국 해결하지 못해 Vitest로 마이그레이션하기로 했다. 그리고 이왕 이렇게 된 것 Webpack말고 Vite도 사용해보고 싶어서 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

Vitest와 RTL 세팅


설정 파일 세팅

/// <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();
    });
});

MSW 세팅


Browser와 Node 세팅으로 나누는 이유

맨처음에는 서비스 워커를 이용해 모킹하는 Browser 세팅만 했더니 브라우저에서 모킹 테스트는 잘 됐지만, VSC에서 Vitest와 RTL과 결합한 테스트는 안되는 현상이 발생했다. 그래서 Node 세팅을 하니 이번에는 VSC에서는 잘 되지만 브라우저에서는 에러가 뜨는 현상이 발생했다. 고민을 해보니 그냥 둘다 세팅하면 되지 않나? 해서 둘 다 세팅을 했다.

handler 세팅

// 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 세팅이다.

Node 세팅

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 테스트를 시작하기 전에 서버를 실행하게 끔 한다.

Browser 세팅

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 테스트 코드

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 테스트 코드

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 호출이 실패할 경우를 만들어준다.


테스트가 잘 작동한다.

이제 세팅은 지겨우니까 제발 테스트 코드 좀 짜보자!

Reference


https://velog.io/@megen07/Vite-React-Vitest-MSW-testing-library%EB%A1%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%9E%91%EC%84%B1%ED%95%98%EA%B8%B0

profile
웹 개발자 고기호입니다.

0개의 댓글