2024
May
Stop Mocking Fetch

๐Ÿ”— Stop mocking fetch (opens in a new tab)

๐Ÿ—“๏ธ ๋ฒˆ์—ญ ๋‚ ์งœ: 2024.05.26

๐Ÿงš ๋ฒˆ์—ญํ•œ ํฌ๋ฃจ: ๋Ÿฌ๊ธฐ(๋ฐ•์ •์šฐ)


โ€œfetchโ€ ๋ชจํ‚น์€ ๊ทธ๋งŒ!

// __tests__/checkout.js
import * as React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { client } from "~/utils/api-client";
 
jest.mock("~/utils/api-client");
 
test('clicking "confirm" submits payment', async () => {
  const shoppingCart = buildShoppingCart();
  render(<Checkout shoppingCart={shoppingCart} />);
 
  client.mockResolvedValueOnce(() => ({ success: true }));
 
  userEvent.click(screen.getByRole("button", { name: /confirm/i }));
 
  expect(client).toHaveBeenCalledWith("checkout", { data: shoppingCart });
  expect(client).toHaveBeenCalledTimes(1);
  expect(await screen.findByText(/success/i)).toBeInTheDocument();
});

์ด ํ…์ŠคํŠธ๋Š” ๋‹น์‹ ์ด **โ€˜Checkoutโ€™**๊ณผ โ€˜/checkoutโ€™ ์—”๋“œํฌ์ธํŠธ์˜ ์‹ค์ œ API์™€ ์š”๊ตฌ ์‚ฌํ•ญ์„ ์•Œ์ง€ ๋ชปํ•œ๋‹ค๋ฉด ์ •๋ง๋กœ ๋‹ตํ•  ์ˆ˜ ์—†๋‹ค๋Š” ๊ฒƒ์„ ์ „์ œ๋กœ ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์ ์„ ์–‘ํ•ดํ•ด ์ฃผ์‹ญ์‹œ์˜ค.

์ด ๊ฒฝ์šฐ ๋ฌธ์ œ์  ์ค‘ ํ•˜๋‚˜๋Š”, ํด๋ผ์ด์–ธํŠธ๋ฅผ ๋ชจํ‚นํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ œ๋Œ€๋กœ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ๋Š”์ง€ ์–ด๋–ป๊ฒŒ ์•Œ ์ˆ˜ ์—†๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

ํด๋ผ์ด์–ธํŠธ๊ฐ€ window.fetch๋ฅผ ์ œ๋Œ€๋กœ ํ˜ธ์ถœํ•˜๋Š”์ง€ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ํด๋ผ์ด์–ธํŠธ์˜ API๊ฐ€ ์ตœ๊ทผ์— ๋ฐ์ดํ„ฐ ๋Œ€์‹  ๋ณธ๋ฌธ์„ ๋ฐ›๋„๋ก ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์•˜๋‹ค๋Š” ๊ฒƒ์„ ์–ด๋–ป๊ฒŒ ์•Œ ์ˆ˜ ์žˆ์„๊นŒ์š”?

TypeScript๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ์ผ๋ถ€ ๋ฒ„๊ทธ ์นดํ…Œ๊ณ ๋ฆฌ๋Š” ์ œ๊ฑฐ๋˜์—ˆ์Šต๋‹ˆ๋‹ค! (opens in a new tab) ํ•˜์ง€๋งŒ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๋ฒ„๊ทธ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ํด๋ผ์ด์–ธํŠธ๋ฅผ ๋ชจํ‚นํ•˜๋Š” ๋ฐ ์ฃผ์˜๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๋ฌผ๋ก  E2E ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ํ™•์‹ ์„ ์–ป์„ ์ˆ˜ ์žˆ์ง€๋งŒ, ๋” ๋น ๋ฅธ ํ”ผ๋“œ๋ฐฑ ๋ฃจํ”„๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ์ด ํ•˜์œ„ ๋ ˆ๋ฒจ์—์„œ ํด๋ผ์ด์–ธํŠธ์— ์ง์ ‘ ํ˜ธ์ถœํ•˜์—ฌ ํ™•์‹ ์„ ์–ป๋Š” ๊ฒƒ์ด ๋” ๋‚˜์„ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ๋งŒ์•ฝ ๊ทธ๋ ‡๊ฒŒ ์–ด๋ ต์ง€ ์•Š๋‹ค๋ฉด์š”!

ํ•˜์ง€๋งŒ ์‹ค์ œ๋กœ โ€˜fetchโ€™ ์š”์ฒญ์„ ํ•˜๊ณ  ์‹ถ์ง€๋Š” ์•Š๊ฒ ์ฃ ? ๊ทธ๋Ÿผ โ€˜window.fetchโ€™๋ฅผ ๋ชจํ‚นํ•ด ๋ด…์‹œ๋‹ค.

// __tests__/checkout.js
import * as React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
 
beforeAll(() => jest.spyOn(window, "fetch"));
// jest์˜ resetMocks๊ฐ€ "true"๋กœ ์„ค์ •๋˜์–ด ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ฉ๋‹ˆ๋‹ค.
// ์ •๋ฆฌ์— ๋Œ€ํ•ด ๊ฑฑ์ •ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
// ๋˜ํ•œ `whatwg-fetch`์™€ ๊ฐ™์€ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ํด๋ฆฌํ•„์„ ๋กœ๋“œํ–ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ฉ๋‹ˆ๋‹ค.
 
test('clicking "confirm" submits payment', async () => {
  const shoppingCart = buildShoppingCart();
  render(<Checkout shoppingCart={shoppingCart} />);
 
  window.fetch.mockResolvedValueOnce({
    ok: true,
    json: async () => ({ success: true }),
  });
 
  userEvent.click(screen.getByRole("button", { name: /confirm/i }));
 
  expect(window.fetch).toHaveBeenCalledWith(
    "/checkout",
    expect.objectContaining({
      method: "POST",
      body: JSON.stringify(shoppingCart),
    })
  );
  expect(window.fetch).toHaveBeenCalledTimes(1);
  expect(await screen.findByText(/success/i)).toBeInTheDocument();
});

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์‹ค์ œ๋กœ ์š”์ฒญ์ด ์ด๋ฃจ์–ด์ง€๊ณ  ์žˆ์Œ์„ ์ข€ ๋” ํ™•์‹ ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ, ์ด ํ…Œ์ŠคํŠธ์—์„œ ๋ถ€์กฑํ•œ ๊ฒƒ์€ 'Content-Type'์ด 'application/json'์ธ์ง€ ํ™•์ธํ•˜๋Š” ๋‹จ์–ธ์ด ์—†๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๊ฒƒ ์—†์ด ์–ด๋–ป๊ฒŒ ์„œ๋ฒ„๊ฐ€ ๋‹น์‹ ์ด ๋งŒ๋“  ์š”์ฒญ์„ ์ธ์‹ํ•  ๊ฒƒ์ด๋ผ๊ณ  ํ™•์‹ ํ•  ์ˆ˜ ์žˆ๋‚˜์š”? ๊ทธ๋ฆฌ๊ณ  ์˜ฌ๋ฐ”๋ฅธ ์ธ์ฆ ์ •๋ณด๊ฐ€ ์ „์†ก๋˜๊ณ  ์žˆ๋Š”์ง€ ์–ด๋–ป๊ฒŒ ๋ณด์žฅํ•  ์ˆ˜ ์žˆ๋‚˜์š”?

"์šฐ๋ฆฌ๋Š” ํด๋ผ์ด์–ธํŠธ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์—์„œ ์ด๋ฏธ ๊ทธ๊ฒƒ์„ ๊ฒ€์ฆํ–ˆ์–ด์š”, Kent. ๋” ๋ฌด์—‡์„ ์›ํ•˜๋‚˜์š”? ์ €๋Š” ๋ชจ๋“  ๊ณณ์— ๋‹จ์–ธ์„ ๋ณต์‚ฌ/๋ถ™์—ฌ๋„ฃ๊ธฐ ํ•˜๊ณ  ์‹ถ์ง€ ์•Š์•„์š”!" ๋ถ„๋ช…ํžˆ ๋‹น์‹ ์˜ ์ž…์žฅ์„ ์ดํ•ดํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ชจ๋“  ํ…Œ์ŠคํŠธ์—์„œ ๊ทธ ํ™•์‹ ์„ ์–ป์œผ๋ฉด์„œ๋„ ๋ชจ๋“  ๊ณณ์—์„œ ์ถ”๊ฐ€์ ์ธ ์ž‘์—…์„ ํ”ผํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค๋ฉด ์–ด๋–จ๊นŒ์š”? ๊ณ„์† ์ฝ์–ด๋ณด์„ธ์š”.

์ €๋ฅผ ์ •๋ง ๊ดด๋กญํžˆ๋Š” ๊ฒƒ ์ค‘ ํ•˜๋‚˜๋Š” fetch์™€ ๊ฐ™์€ ๊ฒƒ์„ ๋ชจํ‚นํ•  ๋•Œ, ๋‹น์‹ ์€ ํ…Œ์ŠคํŠธ ๊ณณ๊ณณ์—์„œ ์ „์ฒด ๋ฐฑ์—”๋“œ๋ฅผ ์žฌ๊ตฌํ˜„ํ•˜๊ฒŒ ๋œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ข…์ข… ์—ฌ๋Ÿฌ ํ…Œ์ŠคํŠธ์—์„œ ์ด๋Ÿฐ ์ผ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ํŠนํžˆ "์ด ํ…Œ์ŠคํŠธ์—์„œ๋Š” ์ •์ƒ์ ์ธ ๋ฐฑ์—”๋“œ ์‘๋‹ต์„ ๊ฐ€์ •ํ•ฉ๋‹ˆ๋‹ค"๋ผ๊ณ  ํ•  ๋•Œ, ๊ทธ๊ฒƒ์„ ๊ณณ๊ณณ์—์„œ ๋ชจํ‚นํ•ด์•ผ๋งŒ ํ•˜๋Š” ๊ฒƒ์€ ์ •๋ง ์งœ์ฆ๋‚ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ ๊ฒฝ์šฐ์—๋Š” ์ •๋ง๋กœ ๋‹จ์ง€ ์„ค์ • ์†Œ์Œ์ด ๋‹น์‹ ๊ณผ ์‹ค์ œ๋กœ ํ…Œ์ŠคํŠธํ•˜๋ ค๋Š” ๊ฒƒ ์‚ฌ์ด์— ๋ผ์–ด๋“ค๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๊ฒฐ๊ตญ ๋ถˆ๊ฐ€ํ”ผํ•˜๊ฒŒ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์‹œ๋‚˜๋ฆฌ์˜ค ์ค‘ ํ•˜๋‚˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค:

  1. ํด๋ผ์ด์–ธํŠธ๋ฅผ ๋ชจํ‚นํ•ฉ๋‹ˆ๋‹ค (์ฒซ ๋ฒˆ์งธ ํ…Œ์ŠคํŠธ์—์„œ์ฒ˜๋Ÿผ) ๊ทธ๋ฆฌ๊ณ  ๋ช‡ ๊ฐ€์ง€ E2E ํ…Œ์ŠคํŠธ์— ์˜์กดํ•˜์—ฌ ์ ์–ด๋„ ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด ํด๋ผ์ด์–ธํŠธ๋ฅผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Œ์„ ์•ฝ๊ฐ„ ํ™•์‹ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ๋ฐฑ์—”๋“œ๋ฅผ ๊ฑด๋“œ๋ฆฌ๋Š” ๊ฒƒ๋“ค์„ ํ…Œ์ŠคํŠธํ•  ๋•Œ๋งˆ๋‹ค ๋ฐฑ์—”๋“œ๋ฅผ ์žฌ๊ตฌํ˜„ํ•˜๋Š” ๊ฒฐ๊ณผ๋ฅผ ์ดˆ๋ž˜ํ•ฉ๋‹ˆ๋‹ค. ์ข…์ข… ์ž‘์—…์ด ์ค‘๋ณต๋ฉ๋‹ˆ๋‹ค.
  2. window.fetch๋ฅผ ๋ชจํ‚นํ•ฉ๋‹ˆ๋‹ค (๋‘ ๋ฒˆ์งธ ํ…Œ์ŠคํŠธ์—์„œ์ฒ˜๋Ÿผ). ์ด ๋ฐฉ๋ฒ•์€ ์กฐ๊ธˆ ๋‚˜์€ ํŽธ์ด์ง€๋งŒ, #1๊ณผ ๊ฐ™์€ ๋ฌธ์ œ๋ฅผ ์ผ๋ถ€ ๊ฒช์Šต๋‹ˆ๋‹ค.
  3. ๋ชจ๋“  ๊ฒƒ์„ ์ž‘์€ ํ•จ์ˆ˜๋“ค๋กœ ๋‚˜๋ˆ„๊ณ  ๋ชจ๋‘๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ๋‹จ์œ„ ํ…Œ์ŠคํŠธํ•ฉ๋‹ˆ๋‹ค (์ž์ฒด์ ์œผ๋กœ ๋‚˜์˜์ง€ ์•Š์€ ์ผ) ๊ทธ๋ฆฌ๊ณ  ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค (์ข‹์€ ์ผ์€ ์•„๋‹™๋‹ˆ๋‹ค).

๊ฒฐ๊ตญ ์šฐ๋ฆฌ๋Š” ๋” ๋‚ฎ์€ ํ™•์‹ ๊ณผ ๋” ๋Š๋ฆฐ ํ”ผ๋“œ๋ฐฑ ๋ฃจํ”„, ๋งŽ์€ ์ค‘๋ณต๋œ ์ฝ”๋“œ ๋˜๋Š” ์ด๋“ค์˜ ์กฐํ•ฉ์— ์ง๋ฉดํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์˜ค๋žซ๋™์•ˆ ๋‚˜์—๊ฒŒ ์ž˜ ์ž‘๋™ํ–ˆ๋˜ ํ•œ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์€ fetch๋ฅผ ํ•œ ํ•จ์ˆ˜์—์„œ ๋ชจํ‚นํ•˜๋Š” ๊ฒƒ์ธ๋ฐ, ์ด๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ๋‚ด๊ฐ€ ํ…Œ์ŠคํŠธํ•œ ๋ฐฑ์—”๋“œ์˜ ๋ชจ๋“  ๋ถ€๋ถ„์„ ์žฌ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋‚˜๋Š” ์ด ๋ฐฉ์‹์„ PayPal์—์„œ ์‚ฌ์šฉํ–ˆ๊ณ  ๋งค์šฐ ์ž˜ ์ž‘๋™ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ƒ๊ฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค

// add this to your setupFilesAfterEnv config in jest so it's imported for every test file
import * as users from "./users";
 
async function mockFetch(url, config) {
  switch (url) {
    case "/login": {
      const user = await users.login(JSON.parse(config.body));
      return {
        ok: true,
        status: 200,
        json: async () => ({ user }),
      };
    }
    case "/checkout": {
      const isAuthorized = user.authorize(config.headers.Authorization);
      if (!isAuthorized) {
        return Promise.reject({
          ok: false,
          status: 401,
          json: async () => ({ message: "Not authorized" }),
        });
      }
      const shoppingCart = JSON.parse(config.body);
      // do whatever other things you need to do with this shopping cart
      return {
        ok: true,
        status: 200,
        json: async () => ({ success: true }),
      };
    }
    default: {
      throw new Error(`Unhandled request: ${url}`);
    }
  }
}
 
beforeAll(() => jest.spyOn(window, "fetch"));
beforeEach(() => window.fetch.mockImplementation(mockFetch));
// __tests__/checkout.js
import * as React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
 
test('clicking "confirm" submits payment', async () => {
  const shoppingCart = buildShoppingCart();
  render(<Checkout shoppingCart={shoppingCart} />);
 
  userEvent.click(screen.getByRole("button", { name: /confirm/i }));
 
  expect(await screen.findByText(/success/i)).toBeInTheDocument();
});

๋‚ด ์„ฑ๊ณต ๊ฒฝ๋กœ ํ…Œ์ŠคํŠธ๋Š” ํŠน๋ณ„ํžˆ ํ•  ๊ฒƒ์ด ์—†์Šต๋‹ˆ๋‹ค. ์‹คํŒจ ์‚ฌ๋ก€์— ๋Œ€ํ•œ fetch ๋ชจํ‚น์„ ์ถ”๊ฐ€ํ•  ์ˆ˜๋„ ์žˆ์ง€๋งŒ, ์ด ๋ฐฉ์‹์— ๋Œ€ํ•ด ๋งค์šฐ ๋งŒ์กฑํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด ๋ฐฉ์‹์˜ ์žฅ์ ์€ ๋‚ด ํ™•์‹ ์ด ์ฆ๊ฐ€ํ•˜๊ณ  ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ์— ์ž‘์„ฑํ•ด์•ผ ํ•  ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๊ฐ€ ๋”์šฑ ์ค„์–ด๋“ ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋˜ ์ค‘์— msw๋ฅผ ๋ฐœ๊ฒฌํ–ˆ์Šต๋‹ˆ๋‹ค.

msw (opens in a new tab)๋Š” "Mock Service Worker"์˜ ์•ฝ์ž์ž…๋‹ˆ๋‹ค. ์„œ๋น„์Šค ์›Œ์ปค๋Š” Node์—์„œ ์ž‘๋™ํ•˜์ง€ ์•Š๊ณ , ๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ msw๋Š” ํ…Œ์ŠคํŠธ ๋ชฉ์ ์œผ๋กœ Node์—์„œ๋„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ์•„์ด๋””์–ด๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค: ๋ชจ๋“  ์š”์ฒญ์„ ๊ฐ€๋กœ์ฑ„์„œ ์‹ค์ œ ์„œ๋ฒ„์ฒ˜๋Ÿผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ชจ์˜ ์„œ๋ฒ„๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ œ ๊ฐœ์ธ์ ์ธ ๊ตฌํ˜„์—์„œ ์ด๋Š” json ํŒŒ์ผ์„ ์‚ฌ์šฉํ•˜์—ฌ "๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค"๋ฅผ "์”จ๋”ฉ"ํ•˜๊ฑฐ๋‚˜ faker๋‚˜ test-data-bot๊ณผ ๊ฐ™์€ ๊ฒƒ์„ ์‚ฌ์šฉํ•˜๋Š” "๋นŒ๋”"๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ ๋‹ค์Œ ์„œ๋ฒ„ ํ•ธ๋“ค๋Ÿฌ(Express API์™€ ์œ ์‚ฌ)๋ฅผ ๋งŒ๋“ค๊ณ  ๊ทธ ๋ชจ์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ์ƒํ˜ธ ์ž‘์šฉํ•ฉ๋‹ˆ๋‹ค. ์ด ๋ฐฉ๋ฒ•์€ ๋‚ด ํ…Œ์ŠคํŠธ๋ฅผ ๋น ๋ฅด๊ณ  ์‰ฝ๊ฒŒ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค(์ผ๋‹จ ์„ค์ •์„ ๋งˆ์น˜๋ฉด).

์ด์ „์— nock๊ณผ ๊ฐ™์€ ๊ฒƒ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ด๋Ÿฌํ•œ ์ž‘์—…์„ ํ•ด๋ณธ ์ ์ด ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ msw์— ๋Œ€ํ•ด ๋ฉ‹์ง„ ์  (๊ทธ๋ฆฌ๊ณ  ๋‚˜์ค‘์— ์“ธ ์ˆ˜๋„ ์žˆ๋Š” ๊ฒƒ)์€ ๊ฐœ๋ฐœ ์ค‘์— ๋ธŒ๋ผ์šฐ์ €์—์„œ๋„ ๋™์ผํ•œ "์„œ๋ฒ„ ํ•ธ๋“ค๋Ÿฌ"๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ๋ช‡ ๊ฐ€์ง€ ํฐ ์ด์ ์„ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์—”๋“œํฌ์ธํŠธ๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜์„ ๋•Œ
  • ์—”๋“œํฌ์ธํŠธ๊ฐ€ ๊ณ ์žฅ ๋‚ฌ์„ ๋•Œ
  • ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ์ด ๋Š๋ฆฌ๊ฑฐ๋‚˜ ์กด์žฌํ•˜์ง€ ์•Š์„ ๋•Œ

Mirage (opens in a new tab)์— ๋Œ€ํ•ด ๋“ค์–ด๋ณด์…จ์„ ์ˆ˜ ์žˆ๋Š”๋ฐ, ๋งŽ์€ ๋ถ€๋ถ„์—์„œ ๋น„์Šทํ•œ ์ผ์„ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ (ํ˜„์žฌ๋กœ์„œ๋Š”) mirage๋Š” ํด๋ผ์ด์–ธํŠธ์—์„œ ์„œ๋น„์Šค ์›Œ์ปค๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์œผ๋ฉฐ, msw๋ฅผ ์„ค์น˜ํ–ˆ๋Š”์ง€ ์—ฌ๋ถ€์— ๊ด€๊ณ„์—†์ด ๋„คํŠธ์›Œํฌ ํƒญ์ด ๋™์ผํ•˜๊ฒŒ ์ž‘๋™ํ•œ๋‹ค๋Š” ์ ์ด ๋งˆ์Œ์— ๋“ญ๋‹ˆ๋‹ค. ๊ทธ ์ฐจ์ด์ ์— ๋Œ€ํ•ด ๋” ์•Œ์•„๋ณด์„ธ์š”.

// server-handlers.js
// this is put into here so I can share these same handlers between my tests
// as well as my development in the browser. Pretty sweet!
import { rest } from "msw"; // msw supports graphql too!
import * as users from "./users";
 
const handlers = [
  rest.get("/login", async (req, res, ctx) => {
    const user = await users.login(JSON.parse(req.body));
    return res(ctx.json({ user }));
  }),
  rest.post("/checkout", async (req, res, ctx) => {
    const user = await users.login(JSON.parse(req.body));
    const isAuthorized = user.authorize(req.headers.Authorization);
    if (!isAuthorized) {
      return res(ctx.status(401), ctx.json({ message: "Not authorized" }));
    }
    const shoppingCart = JSON.parse(req.body);
    // do whatever other things you need to do with this shopping cart
    return res(ctx.json({ success: true }));
  }),
];
 
export { handlers };
// test/server.js
import { rest } from "msw";
import { setupServer } from "msw/node";
import { handlers } from "./server-handlers";
 
const server = setupServer(...handlers);
export { server, rest };

(โ†’ 24๋…„ 5์›” ๊ธฐ์ค€ import {rest} from 'msw'๋Œ€์‹  import {http} from 'msw' ์‚ฌ์šฉ)

// test/setup-env.js
// add this to your setupFilesAfterEnv config in jest so it's imported for every test file
import { server } from "./server.js";
 
beforeAll(() => server.listen());
// if you need to add a handler after calling setupServer for some specific test
// this will remove that handler for the rest of them
// (which is important for test isolation):
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

// __tests__/checkout.js
import * as React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
 
test('clicking "confirm" submits payment', async () => {
  const shoppingCart = buildShoppingCart();
  render(<Checkout shoppingCart={shoppingCart} />);
 
  userEvent.click(screen.getByRole("button", { name: /confirm/i }));
 
  expect(await screen.findByText(/success/i)).toBeInTheDocument();
});

fetch ๋ชจํ‚น๋ณด๋‹ค ์ด ๋ฐฉ๋ฒ•์ด ๋” ๋งŒ์กฑ์Šค๋Ÿฌ์šด ์ด์œ ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  1. ๊ฐ€์ ธ์˜ค๊ธฐ ์‘๋‹ต ์†์„ฑ๊ณผ ํ—ค๋”์˜ ๊ตฌํ˜„ ์„ธ๋ถ€ ์‚ฌํ•ญ์— ๋Œ€ํ•ด ๊ฑฑ์ •ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
  2. **โ€˜fetchโ€™**๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋ฌธ์ œ๊ฐ€ ์žˆ์œผ๋ฉด ์„œ๋ฒ„ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š๊ณ  ๋‚ด ํ…Œ์ŠคํŠธ๊ฐ€ (์ •ํ™•ํ•˜๊ฒŒ) ์‹คํŒจํ•˜์—ฌ, ๊ฒฐํ•จ ์žˆ๋Š” ์ฝ”๋“œ๋ฅผ ๋ฐฐํฌํ•˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  3. ์ด์™€ ๋™์ผํ•œ ์„œ๋ฒ„ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๊ฐœ๋ฐœ ์ค‘์—๋„ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!

์ฝ”๋กœ์ผ€์ด์…˜๊ณผ ์˜ค๋ฅ˜/์—ฃ์ง€ ์ผ€์ด์Šค ํ…Œ์ŠคํŠธ์— ๊ด€ํ•˜์—ฌ

์ด ์ ‘๊ทผ ๋ฐฉ์‹์— ๋Œ€ํ•œ ํ•˜๋‚˜์˜ ํ•ฉ๋ฆฌ์ ์ธ ์šฐ๋ ค๋Š” ๋ชจ๋“  ์„œ๋ฒ„ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ํ•œ ๊ณณ์— ๋ชจ์•„๋‘๊ณ , ๊ทธ ์„œ๋ฒ„ ํ•ธ๋“ค๋Ÿฌ์— ์˜์กดํ•˜๋Š” ํ…Œ์ŠคํŠธ๋“ค์ด ์ „ํ˜€ ๋‹ค๋ฅธ ํŒŒ์ผ์— ์œ„์น˜ํ•˜๊ฒŒ ๋˜์–ด ์ฝ”๋กœ์ผ€์ด์…˜์˜ ์ด์ ์„ ์žƒ์–ด๋ฒ„๋ฆฐ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

์šฐ์„ , ์ค‘์š”ํ•˜๊ณ  ํ…Œ์ŠคํŠธ์— ํŠน์œ ํ•œ ์š”์†Œ๋“ค๋งŒ ์ฝ”๋กœ์ผ€์ด์…˜ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ์„ค์ •์„ ๊ฐ ํ…Œ์ŠคํŠธ๋งˆ๋‹ค ์ค‘๋ณตํ•ด์„œ ๊ฐ€์งˆ ํ•„์š”๋Š” ์—†์Šต๋‹ˆ๋‹ค. ์œ ์ผํ•œ ๋ถ€๋ถ„๋งŒ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ "์„ฑ๊ณต ๊ฒฝ๋กœ"์— ํ•ด๋‹นํ•˜๋Š” ๊ฒƒ๋“ค์€ ์ผ๋ฐ˜์ ์œผ๋กœ ์„ค์ • ํŒŒ์ผ์— ํฌํ•จ์‹œ์ผœ ํ…Œ์ŠคํŠธ ์ž์ฒด์—์„œ๋Š” ์ œ๊ฑฐํ•˜๋Š” ๊ฒƒ์ด ๋” ๋‚ซ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ๋„ˆ๋ฌด ๋งŽ์€ ์žก์Œ์ด ์ƒ๊ฒจ ์‹ค์ œ๋กœ ๋ฌด์—‡์ด ํ…Œ์ŠคํŠธ๋˜๊ณ  ์žˆ๋Š”์ง€ ๋ถ„๋ฆฌํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.

๊ทธ๋ ‡๋‹ค๋ฉด ์—ฃ์ง€ ์ผ€์ด์Šค์™€ ์˜ค๋ฅ˜์— ๋Œ€ํ•ด์„œ๋Š” ์–ด๋–จ๊นŒ์š”? ์ด ๊ฒฝ์šฐ, MSW๋Š” ํ…Œ์ŠคํŠธ ์ค‘์— ์ถ”๊ฐ€์ ์ธ ์„œ๋ฒ„ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋Ÿฐํƒ€์ž„์— ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋ฉฐ, ์„œ๋ฒ„๋ฅผ ์›๋ž˜์˜ ํ•ธ๋“ค๋Ÿฌ๋กœ ์žฌ์„ค์ •ํ•˜์—ฌ (๋Ÿฐํƒ€์ž„ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ์ œ๊ฑฐํ•˜์—ฌ) ํ…Œ์ŠคํŠธ ๊ฒฉ๋ฆฌ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

// __tests__/checkout.js
import * as React from "react";
import { server, rest } from "test/server";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
 
// happy path test, no special server stuff
test('clicking "confirm" submits payment', async () => {
  const shoppingCart = buildShoppingCart();
  render(<Checkout shoppingCart={shoppingCart} />);
 
  userEvent.click(screen.getByRole("button", { name: /confirm/i }));
 
  expect(await screen.findByText(/success/i)).toBeInTheDocument();
});
 
// edge/error case, special server stuff
// note that the afterEach(() => server.resetHandlers()) we have in our
// setup file will ensure that the special handler is removed for other tests
test("shows server error if the request fails", async () => {
  const testErrorMessage = "THIS IS A TEST FAILURE";
  server.use(
    rest.post("/checkout", async (req, res, ctx) => {
      return res(ctx.status(500), ctx.json({ message: testErrorMessage }));
    })
  );
  const shoppingCart = buildShoppingCart();
  render(<Checkout shoppingCart={shoppingCart} />);
 
  userEvent.click(screen.getByRole("button", { name: /confirm/i }));
 
  expect(await screen.findByRole("alert")).toHaveTextContent(testErrorMessage);
});

๋”ฐ๋ผ์„œ ์ฝ”๋กœ์ผ€์ด์…˜์ด ํ•„์š”ํ•œ ๊ณณ์—๋Š” ์ฝ”๋กœ์ผ€์ด์…˜์„, ์ถ”์ƒํ™”๊ฐ€ ํ•„์š”ํ•œ ๊ณณ์—๋Š” ์ถ”์ƒํ™”๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฒฐ๋ก 

**โ€˜mswโ€™**์™€ ๊ด€๋ จํ•˜์—ฌ ํ•  ์ผ์ด ๋ถ„๋ช…ํžˆ ๋” ๋งŽ์ง€๋งŒ, ์ผ๋‹จ ์—ฌ๊ธฐ์„œ ๋งˆ๋ฌด๋ฆฌํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. **โ€˜mswโ€™**๋ฅผ ์‹ค์ œ๋กœ ๋ณด๊ณ  ์‹ถ๋‹ค๋ฉด, ์ œ๊ฐ€ ์ง„ํ–‰ํ•˜๋Š” 4๋ถ€์ž‘ ์›Œํฌ์ˆ "Build React Apps" (EpicReact.Dev์— ํฌํ•จ)์—์„œ ์‚ฌ์šฉ๋˜๋ฉฐ, GitHub์—์„œ ๋ชจ๋“  ์ž๋ฃŒ๋ฅผ ์ฐพ์•„๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•์˜ ์ •๋ง ๋ฉ‹์ง„ ์ธก๋ฉด ์ค‘ ํ•˜๋‚˜๋Š” ๊ตฌํ˜„ ์„ธ๋ถ€ ์‚ฌํ•ญ์œผ๋กœ๋ถ€ํ„ฐ ๋ฉ€๋ฆฌ ๋–จ์–ด์ ธ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ์ƒ๋‹นํ•œ ๋ฆฌํŒฉํ† ๋ง์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ํ…Œ์ŠคํŠธ๊ฐ€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊นจ๋œจ๋ฆฌ์ง€ ์•Š์•˜๋‹ค๋Š” ํ™•์‹ ์„ ์ค„ ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋ฐ”๋กœ ์ด๊ฒƒ์ด ํ…Œ์ŠคํŠธ๊ฐ€ ์กด์žฌํ•˜๋Š” ์ด์œ ์ž…๋‹ˆ๋‹ค! ์ด๋Ÿฐ ์ผ์ด ์ผ์–ด๋‚  ๋•Œ ์ •๋ง ์ข‹์Šต๋‹ˆ๋‹ค:

Kent C. Dodds

@kentcdodds

์ตœ๊ทผ์— ๋‚ด ์•ฑ์—์„œ ์ธ์ฆ ๋ฐฉ์‹์„ ์™„์ „ํžˆ ๋ณ€๊ฒฝํ–ˆ๋Š”๋ฐ, ํ…Œ์ŠคํŠธ ์œ ํ‹ธ๋ฆฌํ‹ฐ์— ์•ฝ๊ฐ„์˜ ์ˆ˜์ •๋งŒ ํ•„์š”ํ–ˆ๊ณ , ๋ชจ๋“  ํ…Œ์ŠคํŠธ(๋‹จ์œ„, ํ†ตํ•ฉ, E2E)๊ฐ€ ํ†ต๊ณผํ•˜์—ฌ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ๋ณ€๊ฒฝ์— ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š์•˜๋‹ค๋Š” ํ™•์‹ ์„ ์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฐ”๋กœ ์ด๊ฒƒ์ด ํ…Œ์ŠคํŠธ๊ฐ€ ์กด์žฌํ•˜๋Š” ์ด์œ ์ฃ !

Dillon

@d11erh ์˜ค๋Š˜์€ React ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฆฌํŒฉํ† ๋งํ•˜๋Š” ๋ฐ ํ•˜๋ฃจ๋ฅผ ๋ณด๋ƒˆ์Šต๋‹ˆ๋‹ค. react-testing-library๋กœ ์ž˜ ์ž‘์„ฑ๋œ ํ…Œ์ŠคํŠธ(๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค @kentcdodds)๋Š” ํฐ ํ™•์‹ ์„ ์ฃผ์—ˆ๊ณ  ๋ฏธ๋ฌ˜ํ•œ ์˜ค๋ฅ˜๋ฅผ ์žก๋Š” ๋ฐ ๋„์›€์ด ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๊ฒฐ๋ก : ์ข‹์€ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋Š” ์ •๋ง ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค!