가수면

msw 본문

React/테스팅 라이브러리

msw

니비앙 2023. 6. 30. 14:27

https://mswjs.io/docs/

 

Introduction

What is Mock Service Worker?

mswjs.io

실제 요청을 가로채는 API 목킹 라이브러리

 

npm install msw --save-dev

 

기본 세팅

handlers 설정

요청할 api들을 모아 놓은 곳. 원하는 결과를 설정해 넣으면 테스트 시 설정한 대로 요청이 간다.

// src\mocks\handlers.ts

import { rest } from "msw";

export const handlers = [
  rest.get("http://localhost:3030/scoops", (req, res, ctx) => {
    return res(
      ctx.json([
        { name: "Chocolate", imagePath: "/images/chocolate.png" },
        { name: "Vanilla", imagePath: "/images/vanilla.png" },
      ])
    );
  }),
  rest.get("http://localhost:3030/toppings", (req, res, ctx) => {
    return res(ctx.status(200));
  }),
];

server.js 설정

설정한 api 모음을 모킹 서버로 셋업

// src\mocks\server.js

import { setupServer } from 'msw/node';
import { handlers } from './handlers';

// This configures a request mocking server with the given request handlers.
export const server = setupServer(...handlers);

Create React App 했을 시

아래처럼 설정

// src/setupTests.js

import { server } from './mocks/server.js'
// Establish API mocking before all tests.
beforeAll(() => server.listen())

// Reset any request handlers that we may add during the tests,
// so they don't affect other tests.
afterEach(() => server.resetHandlers())

// Clean up after the tests are finished.
afterAll(() => server.close())

그 외

jest.setup.js를 만들어 설정한다.

touch jest.setup.js

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

를 테스트 파일 안에 사용

 

axios 오류 해결

SyntaxError: Cannot use import statement outside a module

 

Jest는 Node.js 환경에서 사용되는 테스트 프레임워크다.

그러나 애플리케이션이 웹 클라이언트용으로 빌드되고, axios가 버전업이 되면서 CommonJS 대신 ES 모듈로 빌드되도록 변경되었기 때문에 CommonJS 형식으로 작성된 Node.js에서 실행되지 않는 문제가 발생한 것이다.

(모듈 외부에서 import 문을 사용할 수 없다며 axios import하는 부분에 오류가 나는 이유)

 

해결 방법으로는 여러가지들이 있다.

1. axios를 0.27버전으로 다운그레이드 한다. (현재 npm을 통해 설치되는 버전은 1.4다...)

npm install axios@^0.27

2. jest, @testing-library/react를 최신 버전으로 업데이트한다. (@testing-library/jest-dom 설치는 권장 사항)

3. jest와 바벨의 설정을 수정한다.

4-1. axios를 CJS 모듈로 가져 오기를 강제한다.

  "scripts": {
  
    "test": "react-scripts test --transformIgnorePatterns \"node_modules/(?!axios)/\"",

  },

4-2. axios를 CJS 모듈로 가져 오기를 강제한다. (이 방법도 안 됐다.)

// package.json

 "jest": {
    "moduleNameMapper": {
      "axios": "axios/dist/node/axios.cjs"
    }
  }

 

내 경우엔 @testing-library/react를 버전 업하고 4-1을 사용하는 방법으로 해결할 수 있었다.

mock api 쿼리 스트링

msw는 쿼리 스트링을 사용하는 url과 사용하지 않는 url을 구분하고 있지 않기 때문에 로직으로 구분해줘야한다.

먼저 예시를 보자.

rest.get("/keywords?type=me", (req, res, ctx) => {
.
.
rest.get("/keywords", (req, res, ctx) => {

만약 위처럼 mock api를 구성할 경우 "/keywords?type=me" 요청이 "/keywords"요청을 덮어쓰게 된다.

이런 경우 아래처럼 로직으로 구분해줘야 한다.

  rest.get("/keywords", (req, res, ctx) => {
    const type = req.url.searchParams.get("type");
    if (type === "me") {
.
.
.
    } else {

실제 환경에서 리액트 쿼리의 invalidateQueries 사용 시 테스트

예를 들어 무언가를 추가할 때 useMutation의 onSuccess로 invalidateQueries를 이용해 get요청을 refetch한다면, msw에서 다음과 같이 res값을 고정된 값으로 get요청을 설정했을 때 refetch가 되어도 고정된 값을 받아와 추가된 것이 반영되지 않으므로 테스트가 실패하게 된다.

  rest.get("/keywords", (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json([
        "리액트네이티브",
        "리액트네이티브 ios",
        "리액트네이티브 애니메이션",
        "리액트네이티브 플러터 차이",
        "리액트네이티브 튜토리얼",
      ]),
    );
  }),
  
    rest.post("/keywords", (req, res, ctx) => {
    return res(ctx.status(200));
  }),

이 경우 다음과 같은 방법으로 해결할 수 있다.

const myKeyword = [
        "리액트네이티브",
        "리액트네이티브 ios",
        "리액트네이티브 애니메이션",
        "리액트네이티브 플러터 차이",
        "리액트네이티브 튜토리얼",
];

//  

rest.get("/keywords", (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json(myKeyword),
    );
  }),
  
  rest.post("/keywords", async (req, res, ctx) => {
    const { keyword } = await req.json();
    myKeyword.push(keyword);
    return res(ctx.status(200));
  }),

use()

현재 서버 인스턴스에 다른 테스트에도 영향을 미치는 요청을 추가 설정한다.

기존 핸들러와 겹치는 경우(동일한 술어를 가짐) 항상 런타임 핸들러가 우선하며, 영구히 대체한다.

import { rest } from 'msw'
import { setupServer } from 'msw/node'

const server = setupServer(
  // Initial request handler for a book detail.
  rest.get('/book/:bookId', (req, res, ctx) => {
    return res(ctx.json({ title: 'Lord of the Rings' }))
  }),
)

test('handles server error gracefully', () => {
  server.use(
    // Runtime request handler override for the "GET /book/:bookId".
    rest.get('/book/:bookId', (req, res, ctx) => {
      return res(ctx.json({ title: 'A Game of Thrones' }))
    }),
  )

  // Any requests to "GET /book/:bookId" will return
  // the "A Game of Thrones" mocked response from
  // the runtime request handler override.
})

일회성으로 다음 요청만 재정의 하려면 return 값에 res.once를 사용하면 된다.

const server = setupServer(
  // Initial request handler for a book detail.
  rest.get('/book/:bookId', (req, res, ctx) => {
    return res(ctx.json({ title: 'Lord of the Rings' }))
  }),
)

test('handles server error gracefully', () => {
  server.use(
    rest.get('/book/:bookId', (req, res, ctx) => {
      // The first matching "GET /book/:bookId" request
      // will receive this mocked response.
      return res.once(
        ctx.status(500),
        ctx.json({ message: 'Internal server error' }),
      )
    }),
  )

  // Request to "GET /book/:bookId" in this test will return
  // a mocked error response (500).
})

test('renders a book detail', () => {
  // Request to "GET /book/:bookId" in this test will return
  // a mocked "Lord of the Rings" book detail.
})

resetHandlers()

handlers에 정의한 모든 api 요청을 재설정한다.

필요한 곳에 resetHandlers을 통해 api를 사용하면 해당 테스트는 디폴트로 설정한 api요청을 덮어씌운다.

const server = setupServer(rest.get('/book/:bookId', bookDetailResolver))

test('first test', () => {
  // "GET /book/:bookId" 사용할 수 있음
})

test('second test', () => {
  server.resetHandlers(rest.post('/login', loginResolver))

  // "POST /login" 만 사용 가능함
})

'React > 테스팅 라이브러리' 카테고리의 다른 글

Playwright 기본  (0) 2024.04.24
테스트 종료와 비동기 업데이트 충돌 오류  (0) 2023.06.27
테스팅 라이브러리 심화  (0) 2023.06.26
user-event  (0) 2023.06.23
테스팅 쿼리  (0) 2023.06.22
Comments