2024
May
Best Practices for Writing Tests with React Testing Library

๐Ÿ”— Best Practices for Writing Tests with React Testing Library (opens in a new tab)

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

๐Ÿงš ๋ฒˆ์—ญํ•œ ํฌ๋ฃจ: ๋งˆ์Šคํ„ฐ์œ„(๋ช…์žฌ์œ„)


Updated On March 12, 2024 Keyword : React, React-Testing-Library, Testing

React Testing Library๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๋ชจ๋ฒ” ์‚ฌ๋ก€

React ํ…Œ์ŠคํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ (opens in a new tab)๋Š” React ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ์‚ฌ์‹ค์ƒ์˜ ํ‘œ์ค€์ด ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž ๊ด€์ ์—์„œ์˜ ํ…Œ์ŠคํŠธ์— ์ง‘์ค‘ํ•˜๊ณ , ํ…Œ์ŠคํŠธ์—์„œ๋Š” ๊ตฌํ˜„ ์„ธ๋ถ€ ์‚ฌํ•ญ์„ ๊ฐ์ถ”๋Š” ๊ฒƒ์ด ์„ฑ๊ณต์˜ ์ฃผ์š” ์ด์œ  ์ค‘ ์ผ๋ถ€์ž…๋‹ˆ๋‹ค.

์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘์„ฑ๋œ ํ…Œ์ŠคํŠธ๋Š” regressions์™€ ๋ฒ„๊ทธ๊ฐ€ ์žˆ๋Š” ์ฝ”๋“œ๋ฅผ ๋ฐฉ์ง€ํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋  ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, React Testing Library์˜ ๊ฒฝ์šฐ ์ปดํฌ๋„ŒํŠธ์˜ ์ ‘๊ทผ์„ฑ (opens in a new tab)๊ณผ ์ „๋ฐ˜์ ์ธ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. React ์ปดํฌ๋„ŒํŠธ๋กœ ์ž‘์—…ํ•  ๋•Œ๋Š” ์ ์ ˆํ•œ ํ…Œ์ŠคํŠธ ๊ธฐ๋ฒ•์„ ์‚ฌ์šฉํ•˜์—ฌ React ํ…Œ์ŠคํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ํ”ํžˆ ๋ฐœ์ƒํ•˜๋Š” ์‹ค์ˆ˜๋ฅผ ํ”ผํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

์ด ๊ธ€์—์„œ๋Š” ํŠน์ • ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ๊ฐœ์„ ํ•˜๋Š” ๋ฐฉ๋ฒ•, *ByRole ์ฟผ๋ฆฌ์˜ ์ค‘์š”์„ฑ, ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์„ ์œ„ํ•ด fireEvent๋ณด๋‹ค userEvent ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•, ๋น„๋™๊ธฐ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด findBy* ์ฟผ๋ฆฌ์™€ waitForElementToBeRemoved๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•๊ณผ ๊ฐ™์€ ์ฃผ์ œ์™€ ํ•จ๊ป˜ React ํ…Œ์ŠคํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ๊ฐ€์žฅ ํ”ํžˆ ์ €์ง€๋ฅด๋Š” ์‹ค์ˆ˜ ๋ช‡ ๊ฐ€์ง€๋ฅผ ๋‹ค๋ฃฐ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด ๊ธ€์ด ๋๋‚˜๋ฉด ๋” ๋‚˜์€ React ํ…Œ์ŠคํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ณ , ์ผ๋ฐ˜์ ์ธ ์‹ค์ˆ˜๋ฅผ ํ”ผํ•˜๊ณ , React ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ „๋ฐ˜์ ์ธ ํ’ˆ์งˆ์„ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ๋Š” ์ง€์‹์„ ๊ฐ–์ถ”๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

Default to *ByRole queries

React ํ…Œ์ŠคํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ๊ฐ•๋ ฅํ•œ ์žฅ์  ์ค‘ ํ•˜๋‚˜๋Š” ์˜ฌ๋ฐ”๋ฅธ ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์˜ˆ์ƒ๋Œ€๋กœ ์ž‘๋™ํ•  ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ ‘๊ทผ์„ฑ (opens in a new tab)๋„ ๋ณด์žฅํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ ‡๋‹ค๋ฉด ์–ด๋–ค ์ฟผ๋ฆฌ๊ฐ€ ๊ฐ€์žฅ ์ข‹์€์ง€ ์–ด๋–ป๊ฒŒ ์•Œ์•„๋‚ผ ์ˆ˜ ์žˆ์„๊นŒ์š”? ๊ทœ์น™์€ ๋งค์šฐ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ *ByRole ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”. ์ด ์ฟผ๋ฆฌ๋Š” ๋งŽ์€ ์š”์†Œ, ์‹ฌ์ง€์–ด ๋ณต์žกํ•œ select components (opens in a new tab)์—์„œ๋„ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

๋Œ€๋ถ€๋ถ„์˜ ๊ทœ์น™๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋ชจ๋“  HTML ์š”์†Œ์— ๊ธฐ๋ณธ ์—ญํ• ์ด ์žˆ๋Š” ๊ฒƒ์€ ์•„๋‹ˆ๋ฏ€๋กœ ์˜ˆ์™ธ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. HTML ์š”์†Œ์˜ ๊ธฐ๋ณธ ์—ญํ•  ๋ชฉ๋ก์€ w3.org (opens in a new tab)์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Testing form components

์ด์ „์˜ ํŠœํ† ๋ฆฌ์–ผ ๊ธ€ (opens in a new tab)์—์„œ ๋ณ€ํ˜•ํ•œ ๋‹ค์Œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

export const Form = ({ saveData }) => {
  const [state, setState] = useState({
    name: "",
    email: "",
    password: "",
    confirmPassword: "",
    conditionsAccepted: false,
  });
 
  const onFieldChange = (event) => {
    let value = event.target.value;
    if (event.target.type === "checkbox") {
      value = event.target.checked;
    }
 
    setState({ ...state, [event.target.id]: value });
  };
 
  const onSubmit = (event) => {
    event.preventDefault();
    saveData(state);
  };
 
  return (
    <form className='form' onSubmit={onSubmit}>
      <div className='field'>
        <label>Name</label>
        <input
          id='name'
          onChange={onFieldChange}
          placeholder='Enter your name'
        />
      </div>
      <div className='field'>
        <label>Email</label>
        <input
          type='email'
          id='email'
          onChange={onFieldChange}
          placeholder='Enter your email address'
        />
      </div>
      <div className='field'>
        <label>Password</label>
        <input
          type='password'
          id='password'
          onChange={onFieldChange}
          placeholder='Password should be at least 8 characters'
        />
      </div>
      <div className='field'>
        <label>Confirm password</label>
        <input
          type='password'
          id='confirmPassword'
          onChange={onFieldChange}
          placeholder='Enter the password once more'
        />
      </div>
      <div className='field checkbox'>
        <input type='checkbox' id='conditions' onChange={onFieldChange} />
        <label>I agree to the terms and conditions</label>
      </div>
      <button type='submit'>Sign up</button>
    </form>
  );
};

form elements๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ ์ž…๋ ฅ์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜๊ณ  ์–‘์‹์„ ์ œ์ถœํ•œ ๋‹ค์Œ saveData prop๊ฐ€ ์ž…๋ ฅํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์‹ ํ–ˆ๋Š”์ง€ ํ™•์ธํ•˜์—ฌ ์ด๋ฅผ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ 3๋‹จ๊ณ„๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  1. ํ…Œ์ŠคํŠธํ•  ํ•„๋“œ์— ํ…์ŠคํŠธ๋ฅผ ์ž…๋ ฅํ•˜๊ฑฐ๋‚˜ ํ™•์ธ๋ž€์„ ํด๋ฆญํ•ฉ๋‹ˆ๋‹ค.
  2. Sign up ๋ฒ„ํŠผ์„ ํด๋ฆญํ•ฉ๋‹ˆ๋‹ค.
  3. ์ž…๋ ฅํ•œ ๋ฐ์ดํ„ฐ๋กœ saveData๊ฐ€ ํ˜ธ์ถœ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

์ด workflow๋Š” ์‚ฌ์šฉ์ž๊ฐ€ form๊ณผ ์ƒํ˜ธ ์ž‘์šฉํ•˜๋Š” ๋ฐฉ์‹์„ ๋งค์šฐ ์œ ์‚ฌํ•˜๊ฒŒ ๋ฐ˜์˜ํ•ฉ๋‹ˆ๋‹ค(์ €์žฅ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ ๊ฒ€์‚ฌํ•˜์ง€๋Š” ์•Š์„ ์ˆ˜ ์žˆ์Œ).

Querying by placeholder text

์ฒซ ๋ฒˆ์งธ ์ž…๋ ฅ ํ•„๋“œ์— ์ด๋ฆ„์„ ์ž…๋ ฅํ•˜๋Š” ๊ฒƒ์œผ๋กœ ์‹œ์ž‘ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. name placeholder์ž„์„ ์ ์—ˆ์œผ๋ฏ€๋กœ, ํ•ด๋‹น Inputd์„ ์ด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ querying ํ•˜๋ฉด ์–ด๋–จ๊นŒ์š”?

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom/extend-expect";
 
const defaultData = {
  conditionsAccepted: false,
  confirmPassword: "",
  email: "",
  name: "",
  password: "",
};
 
describe("Form", () => {
  it("should submit correct form data", async () => {
    const user = userEvent.setup();
    const mockSave = jest.fn();
    render(<Form saveData={mockSave} />);
 
    await user.type(screen.getByPlaceholderText("Enter your name"), "Test");
    await user.click(screen.getByText("Sign up"));
 
    expect(mockSave).toHaveBeenLastCalledWith({ ...defaultData, name: "Test" });
  });
});

์ด ๋ฐฉ๋ฒ•๋„ ํšจ๊ณผ๊ฐ€ ์žˆ์ง€๋งŒ ๊ทธ๋ณด๋‹ค ๋” ๋‚˜์€ ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ฒซ์งธ, ์ด ์ ‘๊ทผ ๋ฐฉ์‹์€ placeholder text๋ฅผ label๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ด€ํ–‰์„ ์กฐ์žฅํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋Š” placeholder์˜ ์›๋ž˜ ์šฉ๋„๊ฐ€ ์•„๋‹ˆ๋ฉด์„œ W3C WAI (opens in a new tab)์—์„œ ๊ถŒ์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋‘˜์งธ, ์ ‘๊ทผ์„ฑ ๋ฌธ์ œ๋ฅผ ์—ผ๋‘์— ๋‘๊ณ  ํ…Œ์ŠคํŠธํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

Querying specific components by role

๋Œ€์‹  ์ฟผ๋ฆฌ๋ฅผ getByRole๋กœ ๋Œ€์ฒดํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ๋ฌธ์„œ์— ๋‚˜์™€ ์žˆ๋“ฏ์ด text box์˜ ์—ญํ• ์— ๋”ฐ๋ผ text ์œ ํ˜•์˜ ์ž…๋ ฅ์„ ์ผ์น˜์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์–‘์‹์— ์—ฌ๋Ÿฌ ๊ฐœ์˜ text box๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ ์ด๋ณด๋‹ค ๋” ๊ตฌ์ฒด์ ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‹คํ–‰ํžˆ๋„ ์ฟผ๋ฆฌ๋Š” ์˜ต์…˜ ๊ฐ์ฒด์ธ ๋‘ ๋ฒˆ์งธ ๋งค๊ฐœ ๋ณ€์ˆ˜๋ฅผ ํ—ˆ์šฉํ•˜๋ฏ€๋กœ name ์†์„ฑ์„ ์‚ฌ์šฉํ•˜์—ฌ ์ผ์น˜ํ•˜๋Š” ํ•ญ๋ชฉ์˜ ๋ฒ”์œ„๋ฅผ ์ขํž ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

RTL ๊ณต์‹๋ฌธ์„œ (opens in a new tab)๋ฅผ ๋ณด๋ฉด ์—ฌ๊ธฐ์„œ name์ด ์ž…๋ ฅ์˜ ์ด๋ฆ„ ์†์„ฑ์ด ์•„๋‹ˆ๋ผ accessible name (opens in a new tab)์„ ๊ฐ€๋ฆฌํ‚จ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ž…๋ ฅ์˜ ๊ฒฝ์šฐ accessible name์€ ๋ ˆ์ด๋ธ”์˜ ํ…์ŠคํŠธ ์ฝ˜ํ…์ธ ์ธ ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ ์–‘์‹์—์„œ๋Š” name input์— Name label์ด ์žˆ์œผ๋ฏ€๋กœ ์ด๋ฅผ ์‚ฌ์šฉํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

user.type(screen.getByRole("textbox", { name: "Name" }), "Test");

๊ทผ๋ฐ ํ…Œ์ŠคํŠธ๋ฅผ ๋Œ๋ฆฌ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค:

TestingLibraryElementError: Unable to find an accessible element with the role "textbox" and name "Name"

์•„๋ž˜์˜ help text๋Š” ์ž…๋ ฅ์— ์—‘์„ธ์Šคํ•  ์ˆ˜ ์žˆ๋Š” ์ด๋ฆ„์ด ์—†๋‹ค๋Š” ๊ฒƒ์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

Here are the accessible roles:
  textbox:
  Name "":
  <input
    id="name"
    placeholder="Enter your name"
  />

input์— ๋Œ€ํ•œ label์ด ์žˆ๋Š”๋ฐ ์™œ ์ž‘๋™ํ•˜์ง€ ์•Š์„๊นŒ์š”? label์„ input๊ณผ ์—ฐ๊ฒฐ (opens in a new tab)ํ•ด์•ผ ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๋ฐํ˜€์กŒ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด์„œ๋Š” label์— ์—ฐ๊ฒฐ๋œ input์˜ ID์™€ ์ผ์น˜ํ•˜๋Š” for ์†์„ฑ์ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋˜๋Š” input element๋ฅผ label ์•ˆ์— ๋ž˜ํ•‘ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. input์— ์ด๋ฏธ id๊ฐ€ ์žˆ๋Š” ๊ฒƒ ๊ฐ™์œผ๋ฏ€๋กœ for(React๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ htmlFor) ์†์„ฑ์„ ์ถ”๊ฐ€ํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค:

<label htmlFor="name">Name</label>
<input
  id="name"
  onChange={onFieldChange}
  placeholder="Enter your name"
/>

์ด์ œ input์ด label๊ณผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์—ฐ๊ฒฐ๋˜๊ณ  ํ…Œ์ŠคํŠธ๊ฐ€ ํ†ต๊ณผ๋ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ ์ ‘๊ทผ์„ฑ๋„ ํฌ๊ฒŒ ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค. ์ฒซ์งธ, label์„ ํด๋ฆญํ•˜๊ฑฐ๋‚˜ ํƒญํ•˜๋ฉด focus๊ฐ€ ์—ฐ๊ฒฐ๋œ input์œผ๋กœ ๋งž์ถฐ์ง‘๋‹ˆ๋‹ค. ๋‘˜์งธ, ๊ฐ€์žฅ ์ค‘์š”ํ•œ ๊ฒƒ์€ input์— ์ดˆ์ ์ด ๋งž์ถฐ์ง€๋ฉด screen readers๊ฐ€ label์„ ์ฝ์–ด ์‚ฌ์šฉ์ž์—๊ฒŒ input์— ๋Œ€ํ•œ ์ถ”๊ฐ€ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•œ๋‹ค๋Š” ์ ์ž…๋‹ˆ๋‹ค. ์ด๋Š” getByRole๋กœ ์ „ํ™˜ํ•˜๋ฉด ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๊ฐ€ ํ–ฅ์ƒ๋  ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ form component์— ๋Œ€ํ•œ ์ ‘๊ทผ์„ฑ์ด ์–ด๋–ป๊ฒŒ ๊ฐœ์„ ๋˜๋Š”์ง€ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

Improving the button test

ํ…Œ์ŠคํŠธ๋ฅผ ๋‹ค์‹œ ์‚ดํŽด๋ณด๋‹ˆ submit button์— getByText ์ฟผ๋ฆฌ๊ฐ€ ์‚ฌ์šฉ๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ œ ์ƒ๊ฐ์—๋Š” *ByText๊ฐ€ ๊ฐ€์žฅ ๊นจ์ง€๊ธฐ ์‰ฝ๊ธฐ ๋•Œ๋ฌธ์— ์ตœํ›„์˜ ์ˆ˜๋‹จ์œผ๋กœ(๋˜๋Š” *ByTestId๋ณด๋‹ค ๋’ค์—์„œ ๋‘๋ฒˆ์งธ๋กœ) ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด ํ…Œ์ŠคํŠธ์—์„œ screen.getByText("Sign up")๋Š” ํ•ด๋‹น element๋ฅผ Sigh up text content๊ฐ€ ์žˆ๋Š” ํ…์ŠคํŠธ ๋…ธ๋“œ์™€ match์‹œํ‚ต๋‹ˆ๋‹ค. ๋‚˜์ค‘์— ๊ฐ™์€ ํŽ˜์ด์ง€์— "Sign up"์ด๋ผ๋Š” ํ…์ŠคํŠธ๊ฐ€ ์žˆ๋Š” ๋‹จ๋ฝ์„ ์ถ”๊ฐ€ํ•˜๊ธฐ๋กœ ๊ฒฐ์ •ํ•˜๋ฉด ํ•ด๋‹น element๋„ matchํ•˜๊ฒŒ ๋˜๊ณ , ์—ฌ๋Ÿฌ ๊ฐœ์˜ matching element๋กœ ์ธํ•ด ํ…Œ์ŠคํŠธ๊ฐ€ ์ค‘๋‹จ๋ฉ๋‹ˆ๋‹ค. ๋ฌธ์ž์—ด ๋Œ€์‹  ์ผ๋ฐ˜ ์ •๊ทœ์‹์„ ํ…์ŠคํŠธ ์ผ์น˜์— ์‚ฌ์šฉํ•˜๋ฉด ์ƒํ™ฉ์ด ๋” ์•…ํ™”๋ฉ๋‹ˆ๋‹ค: screen.getByText(/Sign up/i). ์ด ๊ฒฝ์šฐ ๋Œ€์†Œ๋ฌธ์ž์— ๊ด€๊ณ„์—†์ด 'sign up'์ด๋ผ๋Š” ๋ฌธ์ž์—ด์ด ๋” ํฐ ๋ฌธ์žฅ์˜ ์ผ๋ถ€์ด๋”๋ผ๋„ ๋ชจ๋‘ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค.

์ด ํŠน์ • ๋ฌธ์ž์—ด๋งŒ ์ผ์น˜ํ•˜๋„๋ก ์ •๊ทœ์‹์„ ์ˆ˜์ •ํ•  ์ˆ˜๋„ ์žˆ์ง€๋งŒ, ๋Œ€์‹  ๋” ์ •ํ™•ํ•œ ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ ๋™์‹œ์— *ByRole ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ form์— ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ ์ •ํ™•ํ•œ ์ฟผ๋ฆฌ๋Š” screen.getByRole("button", { name: "Sign up" }); ์ž…๋‹ˆ๋‹ค. ์ด๋ฒˆ์— ์•ก์„ธ์Šค ๊ฐ€๋Šฅํ•œ ์ด๋ฆ„์€ ๋ฒ„ํŠผ์˜ ์‹ค์ œ text content์ž…๋‹ˆ๋‹ค. ๋ฒ„ํŠผ์— aria-label์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์•ก์„ธ์Šค ๊ฐ€๋Šฅํ•œ ์ด๋ฆ„์€ ํ•ด๋‹น aria-label์˜ text content๊ฐ€ ๋ฉ๋‹ˆ๋‹ค.

์ตœ์ข…์ ์œผ๋กœ ์—…๋ฐ์ดํŠธ๋œ ํ…Œ์ŠคํŠธ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

describe("Form", () => {
  it("should submit correct form data", async () => {
    const user = userEvent.setup();
    const mockSave = jest.fn();
    render(<Form saveData={mockSave} />);
 
    await user.type(screen.getByRole("textbox", { name: "Name" }), "Test");
    await user.click(screen.getByRole("button", { name: "Sign up" }));
 
    expect(mockSave).toHaveBeenLastCalledWith({ ...defaultData, name: "Test" });
  });
});

form component๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ธฐ ์œ„ํ•ด React-Testing-Library๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์— ๊ด€์‹ฌ์ด ์žˆ๋‹ค๋ฉด ์ด ๋ฌธ์„œ๊ฐ€ ๋„์›€์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค: Testing React Hook Form With React Testing Library. (opens in a new tab)

*ByRole vs *ByLabelText for input elements

์ž…๋ ฅ ์š”์†Œ์— ๋Œ€ํ•ด *ByRole ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ์— ๋Œ€ํ•ด ๊ถ๊ธˆํ•ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ ๋ชฉ์ ์€ input element๋ฅผ ํ•ด๋‹น label๊ณผ ์—ฐ๊ด€์ง€์–ด ๋งค์นญํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ถ๊ทน์ ์œผ๋กœ ๊ฐ™์€ ๋ชฉํ‘œ๋ฅผ ๋‹ฌ์„ฑํ•˜๊ณ  ๋ฌธ๋ฒ•๋„ ๋” ๊ฐ„๋‹จํ•œ *ByLabelText ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋” ์‰ฝ์ง€ ์•Š์„๊นŒ์š”? ๋‘ ์ฟผ๋ฆฌ ์‚ฌ์ด์— ํฐ ์ฐจ์ด๊ฐ€ ์—†์–ด ๋ณด์ผ ์ˆ˜ ์žˆ์ง€๋งŒ, *ByRole ์ฟผ๋ฆฌ๋Š” ์š”์†Œ๋ฅผ ๋งค์นญํ•  ๋•Œ ๋” ๊ฒฌ๊ณ ํ•˜๋ฉฐ (opens in a new tab), <label>์—์„œ aria-label๋กœ ์ „ํ™˜ํ•ด๋„ ์—ฌ์ „ํžˆ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

๋ฐ˜๋ฉด, ๋ชจ๋“  ์œ ํ˜•์˜ input ์š”์†Œ๊ฐ€ ๊ธฐ๋ณธ role์„ ๊ฐ€์ง€๋Š” ๊ฒƒ์€ ์•„๋‹™๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, password๋‚˜ email input์˜ ๊ฒฝ์šฐ, *ByLabelText ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ๋‘ ๋ฐฉ๋ฒ• ๋ชจ๋‘ ์žฅ์ ์ด ์žˆ์ง€๋งŒ, ํŠน์ • ์‚ฌ์šฉ ์‚ฌ๋ก€๋ฅผ ๊ณ ๋ คํ•˜๊ณ  ์ƒํ™ฉ์— ๊ฐ€์žฅ ์ ํ•ฉํ•œ ์ฟผ๋ฆฌ๋ฅผ ์„ ํƒํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

Use userEvent instead of fireEvent

์šฐ๋ฆฌ๋Š” ์ด๋ฒคํŠธ๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜๊ธฐ ์œ„ํ•ด ๋‚ด์žฅ๋œ fireEvent ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ๋Œ€์‹  userEvent (opens in a new tab) ๋ฉ”์„œ๋“œ๋ฅผ ๊ธฐ๋ณธ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๋ˆˆ์น˜์ฑ˜์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. fireEvent๊ฐ€ ๋งŽ์€ ๊ฒฝ์šฐ์— ์ž‘๋™ํ•˜์ง€๋งŒ, ์ด๋Š” dispatchEvent API ์œ„์— ๊ฐ€๋ฒผ์šด ๋ž˜ํผ๋กœ์„œ ์‚ฌ์šฉ์ž ์ƒํ˜ธ์ž‘์šฉ์„ ์™„์ „ํžˆ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ฐ˜๋ฉด์—, userEvent๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ๊ฒƒ๊ณผ ๋™์ผํ•œ ๋ฐฉ์‹์œผ๋กœ DOM์„ ์กฐ์ž‘ํ•˜์—ฌ ๋” ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ํ…Œ์ŠคํŠธ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ, ์ด๋Ÿฌํ•œ ์ ‘๊ทผ ๋ฐฉ์‹์€ React Testing Library์˜ ์ฒ ํ•™๊ณผ๋„ ๋” ์ž˜ ๋งž๊ณ , ๋ฌธ๋ฒ•์ด ๋” ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค.

testing-library/user-event version 13๋ถ€ํ„ฐ๋Š” ์ด๋ฒคํŠธ๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜๊ธฐ ์ „์— user event๋ฅผ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ด ๊ถŒ์žฅ๋ฉ๋‹ˆ๋‹ค. ์ดํ›„ ๋ฒ„์ „์—์„œ๋Š” ์ด๊ฒƒ์ด ํ•„์ˆ˜๊ฐ€ ๋˜๋ฉฐ, userEvent์—์„œ ์ง์ ‘ ์ด๋ฒคํŠธ๋ฅผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜๋Š” ๊ฒƒ์ด ๋” ์ด์ƒ ์ž‘๋™ํ•˜์ง€ ์•Š์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

userEvent์˜ ์„ค์ •์„ ๊ฐ„์†Œํ™”ํ•˜๊ธฐ ์œ„ํ•ด, ์ด๋ฒคํŠธ ์„ค์ •๊ณผ ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง์„ ๋™์‹œ์— ์ฒ˜๋ฆฌํ•˜๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// setup userEvent
function setup(jsx) {
  return {
    user: userEvent.setup(),
    ...render(jsx),
  };
}
 
describe("Form", () => {
  it("should save correct data on submit", async () => {
    const mockSave = jest.fn();
    const { user } = setup(<Form saveData={mockSave} />);
 
    await user.type(screen.getByRole("textbox", { name: "Name" }), "Test");
    await user.click(screen.getByRole("button", { name: "Sign up" }));
 
    expect(mockSave).toHaveBeenLastCalledWith({ ...defaultData, name: "Test" });
  });
});

๋ชจ๋“  userEvent ๋ฉ”์„œ๋“œ๋Š” ๋น„๋™๊ธฐ์ ์ด๋ฏ€๋กœ, ํ…Œ์ŠคํŠธ๋„ ์•ฝ๊ฐ„ ์กฐ์ •ํ•˜์—ฌ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋งŒ๋“ค์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ, userEvent๋Š” ๋ณ„๋„์˜ ํŒจํ‚ค์ง€์ด๋ฏ€๋กœ npm i -D @testing-library/user-event ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด ์„ค์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Simplify the waitFor queries with findBy*

์ข…์ข… ๋งค์นญํ•˜๋ ค๋Š” ์š”์†Œ๊ฐ€ ์ดˆ๊ธฐ ๋ Œ๋”๋ง ์‹œ์ ์—๋Š” ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, API์—์„œ ํ•ญ๋ชฉ์„ ๊ฐ€์ ธ์˜จ ํ›„์— ์ด๋ฅผ ํ‘œ์‹œํ•  ๋•Œ๊ฐ€ ๊ทธ๋Ÿฌํ•ฉ๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์ƒํ™ฉ์—์„œ๋Š” ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์ „์— ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ชจ๋“  ๋ Œ๋”๋ง ์‚ฌ์ดํด์„ ์™„๋ฃŒํ•˜๋„๋ก ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ListPage ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ˆ˜์ •ํ•˜์—ฌ ๋น„๋™๊ธฐ์ ์œผ๋กœ ํ•ญ๋ชฉ ๋ชฉ๋ก์ด ๋กœ๋“œ๋˜๊ธฐ๋ฅผ ๊ธฐ๋‹ค๋ฆฌ๋„๋ก ํ•ฉ์‹œ๋‹ค:

export const ListPage = () => {
  const [items, setItems] = useState([]);
 
  useEffect(() => {
    const loadItems = async () => {
      setTimeout(() => setItems(["Item 1", "Item 2"]), 100);
    };
    loadItems();
  }, []);
 
  if (!items.length) {
    return <div>Loading...</div>;
  }
 
  return (
    <div className='text-list__container'>
      <h1>List of items</h1>
      <ItemList items={items} />
    </div>
  );
};

ํ˜„์žฌ ์ปดํฌ๋„ŒํŠธ์— ๋Œ€ํ•œ ํ…Œ์ŠคํŠธ๋Š” screen.getByRole ์ฟผ๋ฆฌ๊ฐ€ ํ˜ธ์ถœ๋  ๋•Œ ๋กœ๋”ฉ ํ…์ŠคํŠธ๋งŒ ํ‘œ์‹œ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋” ์ด์ƒ ์ž‘๋™ํ•˜์ง€ ์•Š์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋กœ๋”ฉ์„ ์™„๋ฃŒํ•  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฌ๊ธฐ ์œ„ํ•ด waitFor helper๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ์€ ์ˆ˜์ •๋œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค:

import { waitFor } from "@testing-library/react";
 
//...
 
describe("ListPage", () => {
  it("renders without breaking", async () => {
    render(<ListPage />);
 
    await waitFor(() => {
      expect(
        screen.getByRole("heading", { name: "List of items" })
      ).toBeInTheDocument();
    });
  });
});

Enzyme์—์„œ React Testing Library๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•˜๋Š” ๊ฒƒ์— ๊ด€์‹ฌ์ด ์žˆ๋‹ค๋ฉด, "Enzyme vs React Testing Library: A Migration Guide" (opens in a new tab)๋ผ๋Š” ์•„ํ‹ฐํด์ด ๋„์›€์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ์˜ ๋ฐฉ์‹๋„ ์ž‘๋™ํ•˜์ง€๋งŒ, ๋น„๋™๊ธฐ ๋™์ž‘์ด ๋‚ด์žฅ๋œ ์ฟผ๋ฆฌ ํƒ€์ž…์ด ์žˆ์Šต๋‹ˆ๋‹ค: findBy* queries. ์ด ์ฟผ๋ฆฌ๋“ค์€ waitFor์˜ ๋ž˜ํผ๋กœ์„œ, ํ…Œ์ŠคํŠธ๋ฅผ ๋” ์ฝ๊ธฐ ์‰ฝ๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค:

describe("ListPage", () => {
  it("renders without breaking", async () => {
    render(<ListPage />);
    expect(
      await screen.findByRole("heading", { name: "List of items" })
    ).toBeInTheDocument();
  });
});

์ฃผ๋ชฉํ•  ์ ์€ ํ…Œ์ŠคํŠธ ๋ธ”๋ก๋‹น ํ•˜๋‚˜์˜ await ํ˜ธ์ถœ์ด ์ผ๋ฐ˜์ ์œผ๋กœ ์ถฉ๋ถ„ํ•˜๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ๋ชจ๋“  ๋น„๋™๊ธฐ ์ž‘์—…์ด ๊ทธ ์‹œ์ ๊นŒ์ง€ ํ•ด๊ฒฐ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์œ„ ์˜ˆ์ œ์—์„œ ์ถ”๊ฐ€๋กœ ItemList์— 4๊ฐœ์˜ item์ด ์žˆ๋Š”์ง€ ํ…Œ์ŠคํŠธํ•˜๊ณ ์ž ํ•œ๋‹ค๋ฉด, ๋น„๋™๊ธฐ findBy* ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•  ํ•„์š” ์—†์ด getBy* ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ ์˜ˆ์‹œ๋Š” ์›๋ฌธ์—๋Š” ์—†์ง€๋งŒ ์ง์ ‘ ์ถ”๊ฐ€ํ•œ ์˜ˆ์‹œ์ž„์„ ์•Œ๋ ค๋“œ๋ฆฝ๋‹ˆ๋‹ค.

describe("ListPage", () => {
  it("renders without breaking", async () => {
    render(<ListPage />);
 
    // Wait for the heading to be in the document
    expect(
      await screen.findByRole("heading", { name: "List of items" })
    ).toBeInTheDocument();
 
    // Check that the items are displayed
    const items = screen.getAllByRole("listitem");
    expect(items).toHaveLength(4);
  });
});

Testing element's disappearance

์ด๋Š” ๊ฝค ์˜ˆ์™ธ์ ์ธ ๊ฒฝ์šฐ์ด์ง€๋งŒ, ๋น„๋™๊ธฐ ์ž‘์—… ํ›„์— ์ด์ „์— ์กด์žฌํ•˜๋˜ ์š”์†Œ๊ฐ€ DOM์—์„œ ์ œ๊ฑฐ๋˜์—ˆ๋Š”์ง€ ํ…Œ์ŠคํŠธํ•˜๊ณ ์ž ํ•  ๋•Œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. React Testing Library๋Š” ์ด๋ฅผ ์œ„ํ•ด ์œ ์šฉํ•œ helper ํ•จ์ˆ˜์ธ waitForElementToBeRemoved๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ListItem ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฆฌ์ŠคํŠธ ํ—ค๋”๊ฐ€ ๋‚˜ํƒ€๋‚  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฌ๋Š” ๋Œ€์‹ , Loading... ํ…์ŠคํŠธ๊ฐ€ ์ œ๊ฑฐ๋  ๋•Œ๊นŒ์ง€ ๊ธฐ๋‹ค๋ฆฌ๊ณ  ์‹ถ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

it("renders without breaking", async () => {
  render(<ListPage />);
  await waitForElementToBeRemoved(() => screen.queryByText("Loading..."));
});

Use React Testing Library Playground

์–ด๋–ค ์š”์†Œ์— ๋Œ€ํ•ด ์˜ฌ๋ฐ”๋ฅธ ์ฟผ๋ฆฌ๋ฅผ ์ฐพ๋Š” ๋ฐ ์–ด๋ ค์›€์„ ๊ฒช๋Š”๋‹ค๋ฉด, React Testing Library Playground (opens in a new tab)๊ฐ€ ํฐ ๋„์›€์ด ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ์ค‘์ธ ์ปดํฌ๋„ŒํŠธ์˜ HTML์„ ๊ฐ„๋‹จํžˆ ๋ถ™์—ฌ๋„ฃ์œผ๋ฉด ๊ฐ ์š”์†Œ์— ๋Œ€ํ•ด ์ ํ•ฉํ•œ ์ฟผ๋ฆฌ์— ๋Œ€ํ•œ ์œ ์šฉํ•œ ์ œ์•ˆ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด ๋„๊ตฌ๋Š” ๋ณต์žกํ•œ ์ปดํฌ๋„ŒํŠธ์˜ ๊ฒฝ์šฐ ํŠนํžˆ ์ข‹์€๋ฐ, ์–ด๋–ค ์ฟผ๋ฆฌ๊ฐ€ ๊ฐ€์žฅ ์ ํ•ฉํ•œ์ง€ ํ•ญ์ƒ ๋ช…ํ™•ํ•˜์ง€ ์•Š์„ ๋•Œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

Fixing the "not wrapped in act(...)" warnings

๋น„๋™๊ธฐ ๋กœ์ง์„ ํฌํ•จํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์™€ ์ž‘์—…ํ•  ๋•Œ, ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๋™์•ˆ ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€๋ฅผ ์ ‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Warning: An update to ComponentName inside a test was not wrapped in act(...).

์ด ๊ฒฝ๊ณ ๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ React๊ฐ€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฐฉ์‹์„ ์ •ํ™•ํ•˜๊ฒŒ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜์ง€ ๋ชปํ•  ์ˆ˜ ์žˆ์Œ์„ ์‹œ์‚ฌํ•˜๋ฉฐ, ์ด๋Š” ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ์—์„œ false positives ๋˜๋Š” negatives์„ ์ดˆ๋ž˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝ๊ณ ๋Š” ํ…Œ์ŠคํŠธ๊ฐ€ React Testing Library์—์„œ ์ œ๊ณตํ•˜๋Š” act ํ•จ์ˆ˜ ์™ธ๋ถ€์—์„œ ์ƒํƒœ ์—…๋ฐ์ดํŠธ๋‚˜ ๋ถ€์ž‘์šฉ์„ ํŠธ๋ฆฌ๊ฑฐํ•˜์—ฌ React๊ฐ€ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์—…๋ฐ์ดํŠธํ•  ๋•Œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

์ข…์ข… ์ด ๊ฒฝ๊ณ ์˜ ์›์ธ์€ ๋น„๋™๊ธฐ ์ž‘์—… ํ›„ ์š”์†Œ๋‚˜ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๋Š” ๊ฒฝ์šฐ์— getBy* ์ฟผ๋ฆฌ ๋Œ€์‹  findBy* ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ "act" ๊ฒฝ๊ณ ๊ฐ€ ์ •๋‹นํ•˜๋ฉฐ ํ…Œ์ŠคํŠธ์—์„œ false positives ๋˜๋Š” negatives์„ ์ˆ˜์ •ํ•˜๊ธฐ ์œ„ํ•ด ํ•ด๊ฒฐํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌํ•œ ๊ฒฝ์šฐ ์ค‘ ํ•˜๋‚˜๋Š” Jest ํƒ€์ด๋จธ์™€ ์ž‘์—…ํ•  ๋•Œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ด๋จธ๊ฐ€ ์žˆ๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ…Œ์ŠคํŠธํ•  ๋•Œ๋Š” ํƒ€์ด๋จธ์˜ ํ๋ฆ„์„ ์ œ์–ดํ•˜๊ณ  ๋ถˆ์ผ์น˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๊ฐ€์งœ ํƒ€์ด๋จธ(์˜ˆ: Jest์˜ useFakeTimers)๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ํƒ€์ด๋จธ๋ฅผ ์‹คํ–‰ํ•˜๋ ค๋ฉด jest.runAllTimers();๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋Š” ๋˜ํ•œ act๋กœ ๊ฐ์‹ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค.

jest.useFakeTimers();
// ... Set up the tests

act(() => {
  jest.runAllTimers();
});

// ... Do assertions

jest.useRealTimers();

์ด ๊ฒฝ๊ณ ๋Š” React 18์—์„œ ๋” ์ž์ฃผ ๋‚˜ํƒ€๋‚  ์ˆ˜ ์žˆ๋Š”๋ฐ, ์ด๋Š” useEffect์˜ ์‹คํ–‰ ๋ฐฉ์‹์— ์ผ๋ถ€ ๋ณ€๊ฒฝ (opens in a new tab)๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ด์— ๋”ฐ๋ผ ๋” ๋งŽ์€ ํ…Œ์ŠคํŠธ๋ฅผ act๋กœ ๊ฐ์‹ธ์•ผ ํ•  ํ•„์š”๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

"act" ๊ฒฝ๊ณ ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ๋ณด๋‹ค ํฌ๊ด„์ ์ธ ๊ฐ€์ด๋“œ๋Š” ๋‹ค์Œ ๊ธฐ์‚ฌ๋ฅผ ์ฐธ์กฐํ•˜์‹ญ์‹œ์˜ค: Fix the "not wrapped in act(...)" warning. (opens in a new tab)

Writing smoke tests

๋•Œ๋•Œ๋กœ ์šฐ๋ฆฌ๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง ์ค‘์— ๊นจ์ง€์ง€ ์•Š๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ๊ธฐ๋ณธ์ ์ธ sanity ํ…Œ์ŠคํŠธ๋ฅผ ์›ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ ๊ฐ„๋‹จํ•œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ด…์‹œ๋‹ค:

export const ListPage = () => {
  return (
    <div className="text-list__container">
      <h1>List of items</h1>
      <ItemList />
    </div>
  );
};

๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฌธ์ œ ์—†์ด ๋ Œ๋”๋ง๋˜๋Š”์ง€ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

import { render } from "@testing-library/react";
import React from "react";
 
import { ListPage } from "./ListPage";
 
describe("ListPage", () => {
  it("renders without breaking", () => {
    expect(() => render(<ListPage />)).not.toThrow();
  });
});

์ด ๋ฐฉ์‹์€ ์šฐ๋ฆฌ์˜ ๋ชฉ์ ์— ๋ถ€ํ•ฉํ•˜์ง€๋งŒ, React Testing Library์˜ ๊ธฐ๋Šฅ์„ ์ถฉ๋ถ„ํžˆ ํ™œ์šฉํ•˜์ง€ ๋ชปํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋Œ€์‹ , ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

import { render, screen } from "@testing-library/react";
import React from "react";
 
import { ListPage } from "./ListPage";
 
describe("ListPage", () => {
  it("renders without breaking", () => {
    render(<ListPage />);
 
    expect(
      screen.getByRole("heading", { name: "List of items" })
    ).toBeInTheDocument();
  });
});

๋น„๋ก ์ด๊ฒƒ์€ ๋งค์šฐ ๋‹จ์ˆœํ™”๋œ ์˜ˆ์‹œ์ด์ง€๋งŒ, ์ด ์ž‘์€ ๋ณ€๊ฒฝ์œผ๋กœ ์ธํ•ด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง ์ค‘์— ๊นจ์ง€์ง€ ์•Š์„ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, ํ™”๋ฉด ํŒ๋…๊ธฐ์—์„œ ์ œ๋Œ€๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” List of items๋ผ๋Š” ์ด๋ฆ„์˜ header ์š”์†Œ๋ฅผ ํฌํ•จํ•˜๊ณ  ์žˆ์Œ์„ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Conclusion

์ด ์•„ํ‹ฐํด์—์„œ๋Š” React Testing Library ํ…Œ์ŠคํŠธ๋ฅผ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•œ ๋ช‡ ๊ฐ€์ง€ ๊ธฐ๋ฒ•๊ณผ ๋ชจ๋ฒ” ์‚ฌ๋ก€๋ฅผ ํƒ๊ตฌํ•˜๊ณ , ํ”ผํ•ด์•ผ ํ•  ๊ฐ€์žฅ ์ผ๋ฐ˜์ ์ธ ์‹ค์ˆ˜๋“ค์„ ๋‚˜์—ดํ–ˆ์Šต๋‹ˆ๋‹ค. getByRole ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•จ์œผ๋กœ์จ ํ…Œ์ŠคํŠธ๊ฐ€ ์ข‹์€ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ์ œ๊ณตํ•  ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๊ฐ€์น˜ ์žˆ๋Š” ์ ‘๊ทผ์„ฑ ํ–ฅ์ƒ๋„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Œ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ, fireEvent๋ณด๋‹ค userEvent ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์˜ ์ด์ ๊ณผ ์ตœ์ ์˜ ์‚ฌ์šฉ์„ ์œ„ํ•œ ์„ค์ • ๋ฐฉ๋ฒ•์„ ๋ฐฐ์› ์Šต๋‹ˆ๋‹ค. ๋งˆ์ง€๋ง‰์œผ๋กœ, findBy* ์ฟผ๋ฆฌ์™€ waitForElementToBeRemoved๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋” ๊ฒฌ๊ณ ํ•˜๊ณ  ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•๋„ ์‚ดํŽด๋ณด์•˜์Šต๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ํŒ๊ณผ ์š”๋ น์„ ๋”ฐ๋ฅด๋ฉด ์ฝ๊ธฐ ์‰ฝ๊ณ , ์œ ์ง€๋ณด์ˆ˜ํ•˜๊ธฐ ์‰ฌ์šฐ๋ฉฐ, ๋””๋ฒ„๊น… (opens in a new tab)ํ•˜๊ธฐ ์‰ฌ์šด ๋” ๋‚˜์€ React ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ๋Š” ๊ฐœ๋ฐœ ๊ณผ์ •์˜ ํ•„์ˆ˜์ ์ธ ๋ถ€๋ถ„์ด๋ฉฐ, ์ข‹์€ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๋ฐ ์‹œ๊ฐ„์„ ํˆฌ์žํ•˜๋Š” ๊ฒƒ์€ ์žฅ๊ธฐ์ ์œผ๋กœ ํฐ ์ด์ต์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ธฐ๋ฒ•๊ณผ ๋ชจ๋ฒ” ์‚ฌ๋ก€๋ฅผ ํ†ตํ•ด ์šฐ๋ฆฌ์˜ React ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ฒ ์ €ํ•˜๊ฒŒ ํ…Œ์ŠคํŠธ๋˜๊ณ  ๋†’์€ ํ’ˆ์งˆ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณด์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

References and resources

Beyond Console.log: Debugging Techniques In JavaScript (opens in a new tab) Creating Accessible Form Components with React (opens in a new tab) Kent C. Dodds: Fix the "not wrapped in act(...)" warning (opens in a new tab) MDN: The Label element (opens in a new tab) React Testing Library Playground (opens in a new tab) React Testing Library documentation: ByLabelText query (opens in a new tab) React Testing Library documentation: ByRole query (opens in a new tab) React Testing Library documentation (opens in a new tab) React documentation: How to Upgrade to React 18 (opens in a new tab) Testing Select Components with React Testing Library (opens in a new tab) TypeScript: Typing Form Events In React (opens in a new tab) User Event documentation (opens in a new tab) What is an accessible name? (opens in a new tab) w3.org: HTML-ARIA (opens in a new tab) w3.org: Placeholder text (opens in a new tab)