Intergration 테스트 코드 작성 (React Testing Library, MSW로 작성하기)

Dam·2024년 1월 8일
post-thumbnail

MSW(Mock Service Worker)

MSW는 Mock Service Worker의 약자로, Service Worker API를 사용하여 실제 API 요청을 가로채 mocking을 해주는 라이브러리이다.

import { rest } from "msw";
import { setupServer } from "msw/node";

const mockData = {
	activity: "Go for a walk",
  	type: "relaxation",
  	participants: 1,
  	price: 0,
  	link: "",
  	key: "4286250",
  	accessibility: 0.1
};

const server = setupServer(
	rest.get("https://www.boredapi.com/api/activity", req, res, ctx) => (
    	res(ctx.json(mockData))
    )),
);

beforeAll(() => worker.start());
afterEach(() => worker.resetHandlers());
afterAll(() => worker.stop());

세팅 방법은 간단하다.

이제 테스트 코드를 실행하면 www.boredapi.com/api/activity로 API request가 발생할 때마다 MSW에 의해 인터셉트되고 리턴되는 mockData를 통해 항상 일관된 결과를 테스트할 수 있다.

import { fireEvent, render, waitFor, screen } from "@testing-library/react";

beforeEach(() => {
	render(<App />);
});

// ...

test("모달창에 Loading...이 보이고, 로딩이 다 되면 할 일, 최소 인원, 필요한 금액 텍스트가 보인다.", async () => {
	// 버튼을 누르고 모달창이 뜨면
  	fireEvent.click(screen.getByRole("button", { name: "Let's find!" }));
  
  	// 로딩 화면이 보이고
  	expect(screen.getByText("Loading...")).toBeInTheDocument();
  
  	// 로딩이 완료되면
  	await waitFor(() => screen.getByText(/할 일:/));
  	// ^ 또는 waitForElementToBeRemoved(() => screen.getByText("Loading..."));
  
  	// 할 일, 최소 인원, 필요한 금액의 정보가 보인다.
  	expect(screen.getByText("할 일: Go for a walk").textContent).toBeInTheDocument();
  	expect(screen.getByText("최소 인원: 1").textContent).toBeInTheDocument();
  	expect(screen.getByText("필요한 금액: 0$").textContent).toBeInTheDocument();
});

이렇게 간단하게 하나의 Flow를 테스트할 수 있다.

그렇다면 어떤 요소가 없어지는지 여부는 어떻게 알 수 있을까?

// ...

text("[Close] 버튼을 클릭하면 모달창이 닫힌다", () => {
	// 버튼을 누르면 모달창이 보이고
  	fireEvent.click(screen.getByRole("button", { name: "Let's find!" }));
  	expect(screen.getByTestId("modalContainer")).toBeInTheDocument();
  
  	// 모달창의 닫기 버튼을 누르면
  	fireEvent.click(screen.getByRole("button", { name: "Close" }));
  
  	// 모달창이 화면에서 사라진다.
  	expect(screen.queryByTestId("modalContainer")).not.toBeInTheDocument();
});

여기서 중요한 점은 요소가 사라졌는지 검사할 때는 queryBy-문을 사용한다는 것이다.
왜냐하면 getBy- query문은 요소가 없으면 무조건 에러를 발생시키기 때문이다.
반면 queryBy-는 요소가 없으면 null을 리턴한다.

이제 마지막으로 에러 상황에 대한 테스트 코드를 작성해보자.

test("데이터를 불러오다가 에러가 발생하면 Error라는 글자가 보인다", () => {
	server.use(
      rest.get("https://www.boredapi.com/api/activity", (req, res, ctx) => (
      	res(ctx.status(500))
	  ))      
    );
     
    render(<App />);

	expect(screen.getByText("Loading...")).toBeInTheDocument();

	await waitForElementToBeRomoved(() => screen.getByText("Loading..."));

	expect(screen.getByText("Error")).toBeInTheDocument();
});

특정 테스트 블록에서 API mock response를 바꿔주고 싶을 때는 server.use 함수를 사용한다.
response의 status를 400~500대로 설정하면 에러가 발생한 것처럼 mocking할 수 있다.
이제 화면에 원하는대로 에러 핸들링이 되고 있는지만 테스트해주면 된다.

Intergration 테스트는 어떤 특정한 작은 로직에 대해서만 테스트하는 것이 아니라 앱 전반에 걸쳐 정상적으로 동작하는지 테스트한다.

Intergraion 테스트에서의 중요한 포인트는 유저에게 보여지는 최종 산출에 대해서 테스트해야 한다는 것이다.
즉, 내가 내부 로직을 어떤 방식으로 리팩토링하든 유저에게 전달되는 결과물이 같다면 테스트 코드가 깨져서는 안 된다.
이 원칙을 최대한 반영하여 작성한다면 생각보다 테스트 코드를 유지 보수하는데 큰 리소스가 낭비되지 않을 것이다.

Intergration 테스트 코드를 앱 전반에 작성해두면 기존 코드를 리팩토링하거나, 새로운 기능을 추가한 후 배포할 때 장애가 발생하는 것에 대해 걱정할 필요가 없다.

MSW의 장점

백엔드에 의존하지 않고 프론트엔드 개발을 할 수 있다.

백엔드 개발이 완료되지 않아도 미리 작성된 명세를 토대로 mock API를 만들어두면 나중에 백엔드가 완료되었을 때
base url만 살짝 바꿔주어 쉽게 적용이 가능하다. 따라서 비동기적인 통신 작업이 가능하므로 협업의 효율이 증가한다.
또한, 클라이언트 입장에서 API를 만들어보는 작업이므로 백엔드 개발자에게 클라이언트 시점에서 API에 대한 구체적인 피드백을 줄 수 있다는 장점도 존재한다.

다양한 상황을 시뮬레이션 해볼 수 있다.

실제 데이터를 임의로 설정하거나 응답 속도 등을 임의대로 정하고 싶을 때, 실제 API로는 이를 제어하기가 쉽지 않다.
이 때 MSW를 사용해서 다양한 상황을 시뮬레이션 해볼 수 있다. 이로 인해 훨씬 더 효과적인 예외 처리 작업이 가능하다.

profile
🌐 DOM 위에서 살아남기

0개의 댓글