본문으로 바로가기

 

 

MSW(Mock Service Worker)는 Node.js나 브라우저에서 모두 사용할 수 있는 모킹 라이브러리로, 브라우저에서는 서비스 워커를 활용해 실제 네트워크 요청을 가로채는 방식으로 모킹을 구현합니다. 즉, fetch로 예를 들자면, 브라우저에서는 fetch 요청을 하는 것과 동일하게 네트워크 요청을 수행하고, 이 요청을 중간에 MSW가 감지하고 미리 준비한 모킹 데이터를 제공하는 방식입니다. 이러한 방식은 fetch의 모든 기능을 그대로 사용하면서도 응답에 대해서만 모킹할 수 있으므로 fetch를 모킹하는 것이 훨씬 수월해집니다.

 

MSW를 활용해 fetch 응답을 모킹한 테스트 코드를 다음과 같이 작성해 봤습니다.

import { fireEvent, render, screen } from '@testing-library/react'
import { rest } from 'msw'
import { setupServer } from 'msw/node'

import { FetchComponent } from '.'

const MOCK_TODO_RESPONSE = {
  userId: 1,
  id: 1,
  title: 'delectus aut autem',
  completed: false,
}

const server = setupServer(
  rest.get('/todos/:id', (req, res, ctx) => {
    const todoId = req.params.id

    if (Number(todoId)) {
      return res(ctx.json({ ...MOCK_TODO_RESPONSE, id: Number(todoId) }))
    } else {
      return res(ctx.status(404))
    }
  }),
)

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers());
afterAll(() => server.close())

beforeEach(() => {
  render(<FetchComponent />)
})

describe('FetchComponent 테스트', () => {
  it('데이터를 불러오기 전에는 기본 문구가 뜬다.', async () => {
    const nowLoading = screen.getByText(/불러온 데이터가 없습니다./)
    expect(nowLoading).toBeInTheDocument()
  })

  it('버튼을 클릭하면 데이터를 불러온다.', async () => {
    const button = screen.getByRole('button', { name: /1번/ })
    fireEvent.click(button)

    const data = await screen.findByText(MOCK_TODO_RESPONSE.title)
    expect(data).toBeInTheDocument()
  })

  it('버튼을 클릭하고 서버요청에서 에러가 발생하면 에러문구를 노출한다.', async () => {
    server.use(
      rest.get('/todos/:id', (req, res, ctx) => {
        return res(ctx.status(503))
      }),
    )

    const button = screen.getByRole('button', { name: /1번/ })
    fireEvent.click(button)

    const error = await screen.findByText(/에러가 발생했습니다/)
    expect(error).toBeInTheDocument()
  })
})

 

위의 코드를 조금씩 나눠서 살펴보겠습니다.

const server = setupServer(
  rest.get('/todos/:id', (req, res, ctx) => {
    const todoId = req.params.id

    if (Number(todoId)) {
      return res(ctx.json({ ...MOCK_TODO_RESPONSE, id: Number(todoId) }))
    } else {
      return res(ctx.status(404))
    }
  }),
)

 

이 코드에서는 MSW를 활용해 fetch 응답을 모킹했습니다. setupServer는 MSW에서 제공하는 메서드로, 이름 그대로 서버를 만드는 역할을 합니다. 테스트 코드에서는 라우트 /todos/:id의 요청만 가로채서 todoId가 숫자인지 확인한 다음, 숫자일 때만 MOCK_TODO_RESPONSE와 id를 반환하고, 숫자가 아니라면 404를 반환하도록 코드를 구성했습니다.

 

beforeAll(() => server.listen())
afterEach(() => server.resetHandlers()); // 각 테스트가 실행된 후에 실행되는 후처리기이다.
afterAll(() => server.close())

 

테스트 코드를 시작하기 전에는 서버를 가동하고, 테스트 코드 실행이 종료되면 서버를 종료시킵니다. 한 가지 눈에 띄는 것은 afterEach에 있는 server.resetHandlers()입니다. 이 코드는 앞에서 선언한 setupServer의 기본 설정으로 되돌리는 역할을 합니다. 일반적인 경우라면 필요 없지만, 뒤이어서 작성할 '서버에서 실패가 발생하는 경우'를 테스트할 때는 res를 임의로 ctx.status(503)과 같은 형태로 변경할 것입니다. 그러나 이를 리셋하지 않으면 계속해서 실패하는 코드로 남아있을 것이므로 테스트 실행마다 resetHandlers를 통해 setupServer로 초기화했던 초깃값을 유지하는 것입니다.

 

그 다음부터는 describe를 시작으로 테스트하고 싶은 내용을 테스트 코드로 작성했습니다.

it('버튼을 클릭하면 데이터를 불러온다.', async () => {
    const button = screen.getByRole('button', { name: /1번/ })
    fireEvent.click(button)

    const data = await screen.findByText(MOCK_TODO_RESPONSE.title)
    expect(data).toBeInTheDocument()
  })

 

여기서부터 본격적으로 비동기 이벤트, 버튼을 클릭해 fetch가 발생하는 시나리오를 테스트합니다. 버튼을 클릭하는 것까지는 동일하지만 이후 fetch 응답이 온 뒤에서야 비로소 찾고자 하는 값을 렌더링 할 것입니다. 원하는 값을 동기 방식으로 즉시 찾는 get 메서드 대신, 요소가 렌더링 될 때까지 일정 시간 동안 기다리는 find 메서드를 사용해 요소를 검색했습니다.

 

it('버튼을 클릭하고 서버요청에서 에러가 발생하면 에러문구를 노출한다.', async () => {
    server.use(
      rest.get('/todos/:id', (req, res, ctx) => {
        return res(ctx.status(503))
      }),
    )

    const button = screen.getByRole('button', { name: /1번/ })
    fireEvent.click(button)

    const error = await screen.findByText(/에러가 발생했습니다/)
    expect(error).toBeInTheDocument()
  })

 

앞서 setupServer에서는 정상적인 응답만 모킹 했기 때문에 에러가 발생하는 경우를 테스트하기 어렵습니다. 서버 응답이 실패하는 경우를 테스트하기 위해 server.use를 사용해 기존 setupServer의 내용을 새롭게 덮어씁니다. 여기서는 /todos/:id 라우팅을 모든 경우에 503이 오도록 작성했습니다. 서버 설정이 끝난 이후에는 앞선 테스트와 동일하게 findBy를 활용해 에러 문구가 정상적으로 노출됐는지 확인합니다.

 

server.use를 활용한 서버 기본 작동을 덮어쓰는 작업은 it('버튼을 클릭하고 서버요청에서 에러가 발생하면 에러문구를 노출한다.', async () => {... 에서만 유효해야 합니다. 다른 테스트 시에는 원래대로 서버 작동이 다시 변경되어야 하므로 afterEach에서 resetHandlers를 실행합니다. 이렇게 하면 it 내부의 server.use 구문이 종료된 이후에는 503 에러 라우팅은 사라지고 다시 정상적인 응답만 받을 수 있게 됩니다.

 

물론 앞서 살펴본 테스트가 가장 마지막에 수행하는 테스트이므로 resetHandlers를 제거해도 테스트 결과가 달라지지는 않을 것입니다. 그러나 테스트 케이스가 가장 마지막에 수행되지 않고, resetHandlers를 수행하지 않는다면 다른 테스트 케이스에서도 503 에러를 받게 되므로 주의해야 합니다.

 

지금까지 fetch를 이용한 비동기 컴포넌트를 테스트하는 방법을 알아봤습니다. 여기서 중요한 것은 MSW를 사용한 fetch 응답 모킹과 findBy를 활용해 비동기 요청이 끝난 뒤에 제대로 된 렌더링이 일어났는지 기다린 후에 확인하는 것입니다. 이 두 가지만 염두에 둔다면 비동기 컴포넌트 테스트 또한 크게 다를 것이 없습니다.

 

 

 

References

https://mswjs.io/docs/api/setup-server/reset-handlers/

https://mswjs.io/docs/api/setup-server/listen

https://mswjs.io/docs/api/setup-server/use

React Deep Dive

https://mine-it-record.tistory.com/635