2. React Testing Library

์‹ค์Šต๋ ˆํฌ

React ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉ์ž ์ž…์žฅ์— ๊ฐ€๊น๊ฒŒ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋Š” ๋„๊ตฌ.

when, then์˜ ๊ฐœ๋…์ด ์žˆ๋‹ค.

 context('with two numbers', () => {
    it('returns sum of two numbers', () => {
        
        //when
        const result = add(2)

       //then
       expect(result).toBe(2);
    });
  });

์›๋ž˜ ReactDom..createRoot() ์‹์œผ๋กœ ๋”์„ ๋งŒ๋“ค์–ด์„œ ๋ Œ๋”ํ•ด์คฌ๋‹ค๋ฉด

๊ทธ๋Ÿฐ๊ฑฐ ์—†์ด ํŽธํ•˜๊ฒŒ ํ…Œ์ŠคํŒ… ํ•  ์ˆ˜ ์žˆ๋Š” render๋ฅผ ์ œ๊ณตํ•œ๋‹ค.


// TextField.tsx

import { useRef } from 'react';

type TextFiledProps={
    label:string
    placeholder: string
    text:string
    setFilterText:(value:string) => void
}

export default function TextField({
  label, placeholder, text, setFilterText,
}:TextFiledProps) {
  const id = useRef(`input=${Math.random()}`);

  const handleChange = (e:React.ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target;
    setFilterText(value);
  };

  return (
    <div>
      <label htmlFor="input2">
        {label}
      </label>
      <input
        id="input2"
        type="text"
      />
    </div>

  );
}
//TextField.test.tsx

import { render, screen } from '@testing-library/react';
import TextField from '../src/components/TextField';

test('TextField', () => {
  // given
  const setFilterText = () => {
    //
  };
  // when
  render(
    <TextField
      label="Name"
      placeholder=""
      text=""
      setFilterText={setFilterText}
    />,
  );

  // then ํ™”๋ฉด์ด ๋‚˜์™€์•ผ ํ•จ
  screen.getByLabelText('Name');
  // ๋ ˆ์ด๋ธ”์ด Search์ธ ์ธํ’‹์ด ์žกํžŒ๋‹ค.
});

label text๊ฐ€ Search์ธ ์ธํ’‹์„ ์žก๋Š”๋ฐ,

label์˜ htmlFor์™€ input์˜ id๊ฐ€ ๊ฐ™์•„์•ผ ํ•œ๋‹ค. ๊ทธ๋ž˜์•ผ ๋‘˜์ด ์—ฐ๊ฒฐ๋œ Element๋กœ ์ธ์‹ํ•œ๋‹ค.

๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด getByLabelText๋กœ ์ฐพ์ง€ ๋ชปํ•จ.

์™„์ „ํžˆ ์ผ์น˜ํ•˜์ง€ ์•Š์•„๋„ get์œผ๋กœ ๊ฐ€์ ธ์˜ค๊ณ  ์‹ถ๋‹ค๋ฉด ์ •๊ทœ ํ‘œํ˜„์‹์„ ์ด์šฉํ•œ๋‹ค.

// ex

screen.getByLabelText(/Sear/)

์•„๋ž˜์ฒ˜๋Ÿผ getํ•ด์˜จ element๋ฅผ ํ…Œ์ŠคํŠธ ํ• ์ˆ˜๋„ ์žˆ๋‹ค.


  //TextField.text.tsx
  const input = screen.getByLabelText('Name');
  expect(input.value).toBe('Tester');
  // or screengetByDisplayValue('Tester')

  // TextField.tsx
    return (
    <div>
      <label htmlFor="input2">
        {label}
      </label>
      <input
        id="input2"
        type="text"
        value="Tester"
      />
    </div>

  );

๋ณ€์ˆ˜์ฒ˜๋ฆฌ ํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค

//TextField.test.tsx
test('TextField', () => {
  // given
  const label = 'Name';
  const text = 'Tester';

  const setText = () => {
    //
  };

  // when
  render(
    <TextField
      label={label}
      placeholder=""
      text={text}
      setText={setText}
    />,
  );
  // then 
  screen.getByLabelText('Name');
  screen.getAllByDisplayValue(text); // value๊ฐ€ text์ธ ์ธํ’‹์„ ๊ฐ€์ ธ์˜ด
});



//TextField.tsx
import { useRef } from 'react';

type TextFiledProps={
    label:string
    placeholder: string
    text:string
    setText:(value:string) => void
}

export default function TextField({
  label, placeholder, text, setText,
}:TextFiledProps) {
  const id = useRef(`input=${Math.random()}`);

  const handleChange = (e:React.ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target;
    setFIlter(value);
  };

  return (
    <div>
      <label htmlFor={`${id}`}>
        {label}
      </label>
      <input
        id={`${id}`}
        type="text"
        value={text}
        placeholder={placeholder}
        onChange={handleChange}
      />
    </div>

  );
}

event

์—ฌ๊ธฐ๊นŒ์ง€ ์ƒํƒœ์—์„œ ๋ณด๋ฉด handleChange ๋‚ด์—์„œ setFilter ๋“ฑ ์•„๋ฌด๋Ÿฐ ํ•จ์ˆ˜๋ฅผ ๋„ฃ์–ด๋„

ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜์ง€ ์•Š์•„์„œ ์•„๋ฌด๋Ÿฐ ์ด์ƒ์ด ์—†๋‹ค.

์ด๊ฒƒ๋„ ์žก์•„์ฃผ๊ณ  ์‹ถ๋‹ค.


// TextField.test.tsx
//ํ…Œ์ŠคํŠธ๋ฅผ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

  const input = screen.getByLabelText('Name');
  fireEvent.change(input, {
    target: { value: 'New Name' },
  });


// TextField.tsx

  const handleChange = (e:React.ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target;
    setFilter(value); // ์ด ๋ถ€๋ถ„์ด ์—๋Ÿฌ.
    // ReferenceError: setFilter is not defined
    
    // ์ด๋ฒคํŠธ๊ฐ€ ๋๋‚ฌ์„๋•Œ input ์˜ e.target.value๊ฐ€ ๋ฐ”๋€Œ์–ด์•ผ ํ•˜๋Š”๋ฐ 
    // ๊ทธ๋ ‡๊ฒŒ ํ•  setFilter ํ•จ์ˆ˜๊ฐ€ ์ •์˜๋˜์ง€ ์•Š์•„์„œ.

    // ๊ทธ๋ž˜์„œ setText(value) ๋กœ ๋ฐ”๊ฟ”์ฃผ๋ฉด ์—๋Ÿฌ๋Š” ํ•ด๊ฒฐ๋œ๋‹ค.
    // setText ํ•จ์ˆ˜๋„ ๋‚ด๋ถ€ ๊ตฌํ˜„์ด ๋˜์–ด์žˆ์ง€๋Š” ์•Š์•„๋„ ๋ง์ด๋‹ค.
    
    // setText() ์ด๋ ‡๊ฒŒ value๋ฅผ ์ „๋‹ฌํ•˜์ง€ ์•Š์•„๋„ ์—๋Ÿฌ๋Š” ์—†๋‹ค.
    // ๋”ฑ handleChange ํ•จ์ˆ˜ ์•ˆ์— ์—๋Ÿฌ๊ฐ€ ์žˆ๋Š”์ง€ ์—†๋Š”์ง€๋งŒ ํŒ๋ณ„.
  };

  return (
    <div>
      <label htmlFor={`${id}`}>
        {label}
      </label>
      <input
        id={`${id}`}
        type="text"
        value={text}
        placeholder={placeholder}
        onChange={handleChange}
      />
    </div>
  )

setText๊ฐ€ ๋ถˆ๋ ธ๋Š”์ง€ ํ™•์ธํ•˜๊ณ ์‹ถ์–ด

import { fireEvent, render, screen } from '@testing-library/react';
import TextField from '../src/components/TextField';

test('TextField', () => {
  // given
  const label = 'Name';
  const text = 'Tester';

  let called = false; // ์ถ”๊ฐ€
  const setText = () => { // setText๊ฐ€ ์‹คํ–‰๋˜๋ฉด ์ผ์–ด๋‚  effect 
    called = true;
  };
  // when
  render(
    <TextField
      label={label}
      placeholder="name is harry"
      text={text}
      setText={setText}
    />,
  );

  screen.getByLabelText('Name');
  screen.getAllByDisplayValue(text);
  screen.getAllByPlaceholderText(/name/);

  const input = screen.getByLabelText('Name');
  fireEvent.change(input, {
    target: { value: 'New Name' },
  });

  expect(called).toBeTruthy(); // ์ถ”๊ฐ€ : called ๊ฐ€ true์ผ๋•Œ๋งŒ ํ†ต๊ณผ
});


// TextField.tsx

export default function TextField({
  label, placeholder, text, setText,
}:TextFiledProps) {
  const id = useRef(`input=${Math.random()}`);

  const handleChange = (e:React.ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target;
    // setText(value); 
    // ์ฃผ์„์ฒ˜๋ฆฌ๋กœ ํ•จ์ˆ˜ ์‹คํ–‰๋˜์ง€ ์•Š์•„ ์—๋Ÿฌ๋‚จ.
  };

  return (
    <div>
      <label htmlFor={`${id}`}>
        {label}
      </label>
      <input
        id={`${id}`}
        type="text"
        value={text}
        placeholder={placeholder}
        onChange={handleChange}
      />
    </div>

  );

jest.fn ๋ชฉํ‚น


  let called = false;
  const setText = jest.fn(); 
  // ์ด๋ ‡๊ฒŒ๋งŒ ํ•ด๋„ ๋œ๋‹ค๋Š”๋ฐ ์•ˆ๋˜๋Š”๊ฑธ?

  //๋ฌด์Šจ์ผ์ด ์ผ์–ด๋‚˜๋Š”์ง€ ์ง์ ‘ ์ •์˜ํ•ด๋„ ๋จ.
  const setText = jest.fn(()=>{
    called = true
  }); 

์ด๊ฒƒ๋ณด๋‹ค ๋‹ค๋ฅธ ๋ฐฉ์‹์œผ๋กœ ํ•œ๋‹ค.

setText๊ฐ€ ๋ถˆ๋ ธ๋Š”์ง€, ์›ํ•˜๋Š” ๊ฐ’์œผ๋กœ ๋ฐ”๊พธ๊ฒŒ ํ•˜๋Š”์ง€๊นŒ์ง€ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์‹ถ์–ด

setText๊ฐ€ call๋๋Š”์ง€ ๋ฐ”๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

//TestField.test.tsx
import { fireEvent, render, screen } from '@testing-library/react';
import TextField from '../src/components/TextField';

test('TextField', () => {
  // given
  const label = 'Name';
  const text = 'Tester';

  const setText = jest.fn();

  // when
  render(
    <TextField
      label={label}
      placeholder="name is harry"
      text={text}
      setText={setText}
    />,
  );

//์ด input, fireEvent ๋ถ€๋ถ„์„ ์ฃผ์„ํ•˜๋ฉด toBeCalledWith ๋ถ€๋ถ„์ด ํ†ต๊ณผ๋˜์ง€ ์•Š๋Š”๋‹ค.  fireEvent์— ๋Œ€ํ•ด ์ •ํ™•ํžˆ ๋” ์•Œ์•„๋ณด์ž.
  const input = screen.getByLabelText('Name');
  fireEvent.change(input, {
    target: { value: 'New Name' },
  });

  expect(setText).toBeCalled(); // ์‹คํ–‰์—ฌ๋ถ€
  expect(setText).toBeCalledWith('New Name') // New Name ๋‚˜์˜ค๋Š”์ง€

});

//TextField.tsx
.
.
.

  const handleChange = (e:React.ChangeEvent<HTMLInputElement>) => {
    const { value } = e.target;
    // setText(value);
  };

  return (
    <div>
      <label htmlFor={`${id}`}>
        {label}
.
.
.

context ํ™œ์šฉ

import { fireEvent, render, screen } from '@testing-library/react';
import TextField from '../src/components/TextField';

const context = describe;

describe('TextField', () => {
  // given
  const label = 'Name';
  const text = 'Tester';

  const setText = jest.fn();

  it('renders elements', () => {
    // when
    render(
      <TextField
        label={label}
        placeholder="name is harry"
        text={text}
        setText={setText}
      />,
    );

    // then ํ™”๋ฉด์ด ๋‚˜์™€์•ผ ํ•จ
    screen.getByLabelText(label);
    screen.getAllByDisplayValue(text);
    screen.getAllByPlaceholderText(/name/);
  });

  // --------

  // context: ์ž…๋ ฅํ–ˆ์„๋•Œ
  context('when user enters name', () => {
    it('calls "setText" handler', () => {
      // given
      render(
        <TextField
          label={label}
          placeholder="name is harry"
          text={text}
          setText={setText}
        />,
      );

      // when
      const input = screen.getByLabelText('Name');
      fireEvent.change(input, {
        target: { value: 'New Name' },
      });

      // then
      expect(setText).toBeCalledWith('New Name');
    });
  });
});

์ค‘๋ณต๋˜๋Š” render ๋ถ€๋ถ„์„ ๋นผ์ค€๋‹ค

import { fireEvent, render, screen } from '@testing-library/react';
import TextField from '../src/components/TextField';

const context = describe;

describe('TextField', () => {
  // given
  const label = 'Name';
  const text = 'Tester';

  const setText = jest.fn();

  function renderTextField() {
    render(
      <TextField
        label={label}
        placeholder="name is harry"
        text={text}
        setText={setText}
      />,
    );
  }

  it('renders elements', () => {
    // when
    renderTextField();

    // then
    screen.getByLabelText(label);
    screen.getAllByDisplayValue(text);
    screen.getAllByPlaceholderText(/name/);
  });

  // --------

  context('when user enters name', () => {
    it('calls "setText" handler', () => {
      // given
      renderTextField();

      // when
      const input = screen.getByLabelText('Name');
      fireEvent.change(input, {
        target: { value: 'New Name' },
      });

      // then
      expect(setText).toBeCalledWith('New Name');
    });
  });
});

mock clear

์—ฌ๊ธฐ์„œ it('renders elements')์™€

context๋ฅผ ์ด์šฉํ•œ ๋ถ€๋ถ„ ๋‘ ๊ตฐ๋ฐ์—์„œ ๊ฐ์ž ๋”ฐ๋กœ

setText ๋ชฉ์„ ์‚ฌ์šฉํ•˜๋Š”๋ฐ

์–ด๋””์„œ๋“  ์ฝœ ํ•˜๊ธฐ๋งŒ ํ•˜๋ฉด ์ฝœ ๋œ๊ฑฐ.. ๊ทธ๋Ÿฌ๋ฉด ์•ˆ๋ผ์„œ

๋งค ํ…Œ์ŠคํŠธ๋งˆ๋‹ค ์ดˆ๊ธฐํ™” ํ•ด์ค˜์•ผ ํ•œ๋‹ค.

.
.
.

describe('TextField', () => {
  // given
  const label = 'Name';
  const text = 'Tester';

  const setText = jest.fn();

  beforeEach(() => { //๋‘˜์ค‘์— ๋‚˜ ํ•˜๋ฉด ๋จ.
    setText.mockClear();
    // jest.clearAllMocks();
  });

  function renderTextField() {
    render(
      <TextField
        label={label}
        placeholder="name is harry"
        text={text}
        setText={setText}
      />,
.
.
.

์•„๋ž˜์ฒ˜๋Ÿผ ์ •๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

context ๋‚ด๋ถ€์—์„œ ๋“œ๋Ÿฌ๋‚ด๊ณ  ์‹ถ์œผ๋ฉด ๊ทธ๋ƒฅ ๋“œ๋Ÿฌ๋‚ด๋„ ์ข‹๋‹ค.

import { fireEvent, render, screen } from '@testing-library/react';
import TextField from '../src/components/TextField';

const context = describe;

describe('TextField', () => {
  // given
  const label = 'Name';
  const text = 'Tester';

  const setText = jest.fn();

  beforeEach(() => {
    // setText.mockClear();
    jest.clearAllMocks();
  });

  function renderTextField() {
    render(
      <TextField
        label={label}
        placeholder="name is harry"
        text={text}
        setText={setText}
      />,
    );
  }

  function inputText(value:string) {
    fireEvent.change(screen.getByLabelText('Name'), {
      target: { value },
    });
  }

  it('renders elements', () => {
    // when
    renderTextField();

    // then ํ™”๋ฉด์ด ๋‚˜์™€์•ผ ํ•จ
    screen.getByLabelText(label);
    screen.getAllByDisplayValue(text);
    screen.getAllByPlaceholderText(/name/);
  });

  // --------

  // context: ์ž…๋ ฅํ–ˆ์„๋•Œ
  context('when user enters name', () => {
    beforeEach(() => { //given์ด it ์•ˆ์— ์žˆ๋Š”๊ฒŒ ์‹ซ์–ด์„œ ๋บŒ
      // given
      renderTextField(); 
    });

    it('calls "setText" handler', () => {
      // when
      inputText('New Name'); //renderTextField ์ฒ˜๋Ÿผ ๋นผ์คฌ๋‹ค.

      // then
      expect(setText).toBeCalledWith('New Name');
    });
  });
});

๋ฐ˜๋ณต๋˜๋Š” ์ฝ”๋“œ๋ฅผ Extract Functionํ•˜๊ณ ,

fireEvent ๋“ฑ์„ ํ†ตํ•ด ์ธํ„ฐ๋ž™์…˜๋งŒ ๊ฒ€์ฆํ•œ๋‹ค.

์™ธ๋ถ€ ์˜์กด์„ฑ์ด ํฐ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค๋ฉด ํ•ด๋‹น ๋ถ€๋ถ„๋งŒ ๊ฐ€์งœ๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค

// App.test.tsx
import { render, screen } from '@testing-library/react';
import App from './App';

jest.mock('./hooks/useFetchRestaurants.ts', () => () => [
  {
    id: '1',
    category: '์ค‘์‹',
    name: '๋ฉ”๊ฐ€๋ฐ˜์ ',
    menu: [
      { id: '1', name: '์งœ์žฅ๋ฉด', price: 8000 },
      { id: '2', name: '์งฌ๋ฝ•', price: 8000 },
      { id: '3', name: '์ฐจ๋Œ์งฌ๋ฝ•', price: 9000 },
      { id: '4', name: 'ํƒ•์ˆ˜์œก', price: 14000 },
    ],
  },
]);

test('App', () => {
  render(<App />);

  screen.getByText('๋ฉ”๊ฐ€๋ฐ˜์ ');
});

express ์„œ๋ฒ„๋ฅผ ์‹คํ–‰์‹œํ‚ค์ง€ ์•Š๊ณ  mock์„ ํ™œ์šฉํ•ด ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋‹ค.

์•„๋ž˜์ฒ˜๋Ÿผ ๋”ฐ๋กœ ๋นผ์„œ ๋ชฉ๊ด€๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ๋‹ค.


// fixtures/products.ts
const products = [
  {
    id: '1',
    category: '์ค‘์‹',
    name: '๋ฉ”๊ฐ€๋ฐ˜์ ',
    menu: [
      { id: '1', name: '์งœ์žฅ๋ฉด', price: 8000 },
      { id: '2', name: '์งฌ๋ฝ•', price: 8000 },
      { id: '3', name: '์ฐจ๋Œ์งฌ๋ฝ•', price: 9000 },
      { id: '4', name: 'ํƒ•์ˆ˜์œก', price: 14000 },
    ],
  },
  {
    id: '2',
    category: 'ํ•œ์‹',
    name: '๋ฉ”๋ฆฌ๊น€๋ฐฅ',
    menu: [
      { id: '5', name: '๊น€๋ฐฅ', price: 3500 },
      { id: '6', name: '์ฐธ์น˜๊น€๋ฐฅ', price: 4500 },
      { id: '7', name: '์ œ์œก๊น€๋ฐฅ', price: 5000 },
      { id: '8', name: 'ํ›ˆ์ œ์˜ค๋ฆฌ๊น€๋ฐฅ', price: 5500 },
    ],
  },
  {
    id: '3',
    category: '์ผ์‹',
    name: 'ํ˜น๋“ฑ๊ณ ๋ž˜์นด๋ ˆ',
    menu: [
      { id: '9', name: '๊ธฐ๋ณธ์นด๋ ˆ', price: 9000 },
      { id: '10', name: '๊ฐ€๋ผ์•„๊ฒŒ์นด๋ ˆ', price: 14000 },
      { id: '11', name: '์†Œ์‹œ์ง€์นด๋ ˆ', price: 13000 },
      { id: '12', name: '๋ˆ๊นŒ์Šค์นด๋ ˆ', price: 14000 },
      { id: '13', name: '๋‹ญ๊ฐ€์Šด์‚ด์นด๋ ˆ', price: 13000 },
    ],
  },
];
export default products;


// fixtures/index.ts
import products from './products';

export default {
  products,
};


//App.test.tsx
import { render, screen } from '@testing-library/react';
import fixtures from '../fixtures';
import App from './App';

jest.mock('./hooks/useFetchRestaurants.ts', () => () => fixtures.products);

test('App', () => {
  render(<App />);

  screen.getByText('๋ฉ”๊ฐ€๋ฐ˜์ ');
});

hooks Mock์„ ์•„์˜ˆ ๋”ฐ๋กœ ๋บ„ ์ˆ˜ ์žˆ๋‹ค

// App.test.tsx
import { render, screen } from '@testing-library/react';
import fixtures from '../fixtures';
import App from './App';

jest.mock('./hooks/useFetchRestaurants.ts');
// ํ•จ์ˆ˜๋กœ ์ „๋‹ฌํ–ˆ๋˜ ๋ชฉ์„ ๋”ฐ๋กœ ๋บŒ.

test('App', () => {
  render(<App />);

  screen.getByText('๋ฉ”๊ฐ€๋ฐ˜์ ');
});

// hooks/__mocks__/useFetchRestaurants.ts
import fixtures from '../../../fixtures';
const useFetchProducts = jest.fn(() => fixtures.products);
export default useFetchProducts;

jest.fn์—†์ด () => fixtures.products ๋งŒ ์จ์ค˜๋„ ๊ฐ€๋Šฅ.

๊ทธ๋ ‡์ง€๋งŒ ์จ์ฃผ๋ฉด ๊ฐ€์งœ๋ผ๋Š” ์‚ฌ์‹ค์ด ๋” ๋ช…ํ™•ํ•˜๊ณ 

ํ˜ธ์ถœ์ด ๋๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋“ฑ์˜ test๋ฅผ ๋˜ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ถ™์ด๋Š” ํŽธ์„ ์ถ”์ฒœ.

์ ์  ์•ฑ์ด ์ปค์ง€๋ฉด์„œ ํ•˜๋‚˜์”ฉ ๊ฐ€์งœ ๊ตฌํ˜„์œผ๋กœ ํ•˜๊ธฐ ์–ด๋ ค์šธ ๋•Œ๊ฐ€ ์žˆ๋‹ค.

์ด๋Ÿด๋•Œ MSW๋“ฑ์˜ ๋Œ€์•ˆ์„ ๊ณ ๋ คํ•ด๋ณผ ์ˆ˜ ์žˆ๋‹ค. ๋‹ค์Œ ์ฃผ์ œ๋กœ ๋„˜์–ด๊ฐ€ ๋ณด์ž.

Last updated