본문 바로가기

React/실험실

[React] 벨로퍼트와 함께하는 React Testing - 비동기 작업 테스트

들어가며

벨로퍼트님의 테스팅 튜토리얼을 공부한 내용을 정리하는 글입니다. 

 

비동기적으로 바뀌는 컴포넌트 UI 테스트

const { useState, useEffect } = require("react");

const DelayedToggle = () => {
  const [toggle, setToggle] = useState(false);

  useEffect(() => {
    setInterval(() => {
      setToggle((prev) => !prev);
    }, 1000);
  }, []);

  return (
    <div>
      상태 : <span>{toggle ? "On" : "Off"}</span>
      {toggle && <div>토글이 켜졌다!</div>}
    </div>
  );
};

export default DelayedToggle;

다음과 같이 1초마다 상태 값이 바뀌는 컴포넌트가 있습니다. 

비동기가 포함된 컴포넌트는 어떤 방식으로 테스트를 해야할까요? 

 

Async Utilities 

react-testing-library는 Async Utilities 함수들을 제공해서 비동기 테스트를 할 수 있습니다. 

Async Utilities는 2가지 함수가 있다. 

 

waitFor

waitFor 함수를 사용하면 특정 콜백에서 에러를 발생하지 않을 때 까지 대기할 수 있습니다. 

  test("reveals text when toggle is On", async () => {
    render(<DelayedToggle />);

    const checkToggleText = () => {
      return screen.getByText("토글이 켜졌다!");
    };

    await waitFor(checkToggleText, { timeout: 2000 });
  });

waitFor 함수는 콜백 안의 함수가 에러가 발생하지 않을 때 까지 기다리다가, 대기 시간이 초과되면 

테스트 케이스가 실패한다. 

timeout의 기본값은 4500ms이며, 이는 변경할 수 있다. 

 

waitForElementToBeRemoved

waitForElementToBeRemoved 함수는 특정 엘리먼트가 화면에서 사라질때까지 기다리는

함수이다. 

  test("removes text when toggle is OFF", async () => {
    render(<DelayedToggle />);

    const toggleButton = screen.getByText("토글");
    fireEvent.click(toggleButton);

    screen.getByText("토글이 켜졌다!");

    await waitForElementToBeRemoved(
      () => screen.queryByText("토글이 켜졌다!"),
      { timeout: 2000 }
    );
  });

테스트 방식은 waitFor과 유사하다. 

 

REST  API 호출 시 테스트 

다음으로 REST API를 사용하는 경우 어떤 방식으로 테스트를 해야할까?

테스트를 위해서 먼저 라이브러리를 설치한다. 

yarn add axios

 

JSONPlaceholder에서 제공하는 가짜 API를 사용할 계획이다. 

import axios from "axios";
import { useEffect, useState } from "react";

const RestAPI = ({ id }) => {
  const [userData, setUserData] = useState(null);
  const [loading, setLoading] = useState(false);

  const getUser = async (id) => {
    setLoading(true);

    try {
      const response = await axios.get(
        `https://jsonplaceholder.typicode.com/users/${id}`
      );

      setUserData(response.data);
    } catch (error) {
      console.log(error);
    }

    setLoading(false);
  };

  useEffect(() => {
    getUser(id);
  }, [id]);

  if (loading || !userData) return <div>Loading...</div>;

  const { username, email } = userData;

  return (
    <div>
      <p>UserName : {username}</p>
      <p>email : {email}</p>
    </div>
  );
};

export default RestAPI;

id 값을 props로 받아와서 API를 호출하고 그 결과 username과 email을 보여주는 컴포넌트이다. 

 

REST API를 호출해야 하는 컴포넌트의 경우, 테스트 코드에서도 똑같이 요청을 보낼 수 있지만, 

일반적으로 서버에서 API를 직접 호출하지 않고 mocking한다. 

 

그 이유는 아직 서버의 API가 만들어지지 않은 경우, 무작정 기다리기보단 mocking 서버에서

API 명세에 따라 데이터를 넘겨주는 방식으로 빠르게 개발이 가능하기 때문이다. 

 

mocking으로 테스트하는 방식은 다양하지만 msw 를 사용해서 진행하려고 한다. 

그 이유는 axios-mock-adapter 등 보다 msw를 사용하면 실제 사용자처럼 테스트가 가능하기

때문이다. 

 

실제 사용자처럼 테스트할 수 있다는 것은 axios-mock-adapter 같은 경우 

  mock.onGet('https://jsonplaceholder.typicode.com/users/1').reply(200, {
    id: 1,
    name: 'Leanne Graham',
    username: 'Bret',
    email: 'Sincere@april.biz',
    address: {
      street: 'Kulas Light',
      suite: 'Apt. 556',
      city: 'Gwenborough',
      zipcode: '92998-3874',
      geo: {
        lat: '-37.3159',
        lng: '81.1496'
      }
    },
    phone: '1-770-736-8031 x56442',
    website: 'hildegard.org',
    company: {
      name: 'Romaguera-Crona',
      catchPhrase: 'Multi-layered client-server neural-net',
      bs: 'harness real-time e-markets'
    }
  });

다음과 같은 방식으로 mocking 한다. 

 

하지만 msw 를 사용하면 query, params, body 등에 따라 넘겨주는 방식을 선정할 수 있기 때문에 

실제로 사용한다고 했을 때, 서버 API가 작업이 완료되어 연동하는 경우에 코드를 변경할 필요가

없다는 장점이 있기 때문이다. 

 

msw의 자세한 내용은 앞서 작성해둔 글이 있으므로 빠르게 넘기겠다. 

import { rest } from "msw";

const data = [
  {
    id: "1",
    name: "Leanne Grahqweqweqweam",
    username: "Bret",
    email: "Sincere@april.biz",
    address: {
      street: "Kulas Light",
      suite: "Apt. 556",
      city: "Gwenborough",
      zipcode: "92998-3874",
      geo: {
        lat: "-37.3159",
        lng: "81.1496",
      },
    },
    phone: "1-770-736-8031 x56442",
    website: "hildegard.org",
    company: {
      name: "Romaguera-Crona",
      catchPhrase: "Multi-layered client-server neural-net",
      bs: "harness real-time e-markets",
    },
  },
  // ...
];

const handlers = [
  rest.get(
    `https://jsonplaceholder.typicode.com/users/:id`,
    (req, res, ctx) => {
      const { id: userId } = req.params;

      const result = data.filter(({ id }) => userId === id);

      return res(ctx.status(201), ctx.json(result[0]));
    }
  ),
];

export default handlers;

다음과 같이 handler를 사용할 계획이다. 

 

import { render, screen, waitFor } from "@testing-library/react";
import { setupServer } from "msw/node";
import { rest } from "msw";
import RestAPI from ".";

const data = [
  {
    id: "1",
    name: "Leanne Graham",
    username: "Bret",
    email: "Sincere@april.biz",
    address: {
      street: "Kulas Light",
      suite: "Apt. 556",
      city: "Gwenborough",
      zipcode: "92998-3874",
      geo: {
        lat: "-37.3159",
        lng: "81.1496",
      },
    },
    phone: "1-770-736-8031 x56442",
    website: "hildegard.org",
    company: {
      name: "Romaguera-Crona",
      catchPhrase: "Multi-layered client-server neural-net",
      bs: "harness real-time e-markets",
    },
  },
  {
    id: "2",
    name: "Leanne Graham",
    username: "Bret",
    email: "Sincere@april.biz",
    address: {
      street: "Kulas Light",
      suite: "Apt. 556",
      city: "Gwenborough",
      zipcode: "92998-3874",
      geo: {
        lat: "-37.3159",
        lng: "81.1496",
      },
    },
    phone: "1-770-736-8031 x56442",
    website: "hildegard.org",
    company: {
      name: "Romaguera-Crona",
      catchPhrase: "Multi-layered client-server neural-net",
      bs: "harness real-time e-markets",
    },
  },
];

const server = setupServer(
  rest.get(
    "https://jsonplaceholder.typicode.com/users/:id",
    (req, res, ctx) => {
      const { id: userId } = req.params;

      const result = data.filter(({ id }) => userId === id);

      return res(ctx.status(201), ctx.json(result[0]));
    }
  )
);

describe("REST API", () => {
  beforeAll(() => server.listen());
  afterEach(() => server.resetHandlers());
  afterAll(() => server.close());

  test("calls getUser API loads userData", async () => {
    render(<RestAPI id={1} />);
    await waitFor(function () {
      return screen.getByText("Loading...");
    });
    await waitFor(function () {
      return screen.getByText("Bret");
    });
  });
});

msw 를 react-testing-library와 사용하기 위해서는 setupServer를 사용해야한다.

 

그리고 beforeAll을 통해서 모든 테스트 전에 서버를 실행 시켜주었다. 

afterEach를 통해 종료되기 전 mocking 서버를 초기화 시키고 afterAll를 사용해 모든 테스트 후

mocking 서버를 종료하였다. 

반응형