React Testing Library

ํ•™์Šต ํ‚ค์›Œ๋“œ

  • React Testing Library

  • given - when - then ํŒจํ„ด

  • Mocking

  • Test fixture

React Testing Library

  • Facebook์—์„œ ๊ณต์‹์ ์œผ๋กœ ์‚ฌ์šฉ์„ ๊ถŒ์žฅ (โ†’ React๋กœ ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ ๋‹ค๋ฉด!)

  • Behavior Driven Test(ํ–‰์œ„ ์ฃผ๋„ ํ…Œ์ŠคํŠธ) ๋ฐฉ๋ฒ•๋ก ์ด ๋Œ€๋‘ ๋˜๋ฉด์„œ ํ•จ๊ป˜ ์ฃผ๋ชฉ ๋ฐ›๊ธฐ ์‹œ์ž‘ํ•œ ํ…Œ์ŠคํŒ… ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

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

  • DOM์„ ํ™œ์šฉํ•ด ์šฐ๋ฆฌ๊ฐ€ UI๋ฅผ ์ง์ ‘ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

๐Ÿ“Œ ํ–‰์œ„ ์ฃผ๋„ ํ…Œ์ŠคํŠธ(ํ–‰์œ„ ์ฃผ๋„ ํ…Œ์ŠคํŠธ)๋Š” ๊ธฐ์กด์— ๊ด€ํ–‰์ด๋˜ Implementation Driven Test(๊ตฌํ˜„ ์ฃผ๋„ ํ…Œ์ŠคํŠธ)์˜ ๋‹จ์ ์„ ๋ณด์™„ํ•˜๊ธฐ ์œ„ํ•œ ๋ฐฉ๋ฒ•๋ก ์ด๋ฉฐ ์‚ฌ์šฉ์ž๊ฐ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ด์šฉํ•˜๋Š” ๊ด€์ ์—์„œ ์‚ฌ์šฉ์ž์˜ ์‹ค์ œ ๊ฒฝํ—˜ ์œ„์ฃผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

<h2 class="title">์ œ๋ชฉ</h2>
  • ๊ตฌํ˜„ ์ฃผ๋„ ํ…Œ์ŠคํŠธ ๊ด€์ 

    • h2๋ผ๋Š” ํƒœ๊ทธ๊ฐ€ ์“ฐ์˜€๊ณ , title ์ด๋ผ๋Š” ํด๋ž˜์Šค๊ฐ€ ์‚ฌ์šฉ๋˜์—ˆ๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ํ…Œ์ŠคํŠธ

  • ํ–‰์œ„ ์ฃผ๋„ ํ…Œ์ŠคํŠธ ๊ด€์ 

    • ๋ธŒ๋ผ์šฐ์ € ํ™”๋ฉด์— ์ œ๋ชฉ ์ด๋ผ๋Š” ํ…์ŠคํŠธ๊ฐ€ ๋ณด์ผ ๋ฟ. ๋”ฐ๋ผ์„œ, ์‚ฌ์šฉ์ž์—๊ฒŒ ์–ด๋–ค ์ปจํ…์ธ ๊ฐ€ ํ˜„์žฌ ๋ณด์ด๊ณ , ์‚ฌ์šฉ์ž๊ฐ€ ์–ด๋–ค ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œ์ผฐ์„ ๋•Œ, ๊ทธ์— ๋”ฐ๋ผ ํ™”๋ฉด์— ๋ณ€ํ™”๊ฐ€ ์ผ์–ด๋‚˜๋Š”์ง€๋ฅผ ํ…Œ์ŠคํŠธ

โœ๐Ÿป React Testing Library ์ •๋ฆฌ

โ‡’ ์‚ฌ์šฉ์ž ๊ด€์ ์—์„œ ๋ธŒ๋ผ์šฐ์ €(ํ™”๋ฉด์—) ์ผ์–ด๋‚˜๋Š” ํ–‰์œ„์— ๋Œ€ํ•ด ํ…Œ์ŠคํŠธ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

given - when - then ํŒจํ„ด

  • [์ค€๋น„] - [์‹คํ–‰] - [๊ฒ€์ฆ] ๋‹จ๊ณ„๋กœ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ๋‚˜๋ˆ ์„œ ์ž‘์„ฑํ•œ๋‹ค.

    • Given [์ค€๋น„] : ์‹œ๋‚˜๋ฆฌ์˜ค ์ง„ํ–‰์— ํ•„์š”ํ•œ ๊ฐ’์„ ์„ค์ •, ํ…Œ์ŠคํŠธ์˜ ์ƒํƒœ๋ฅผ ์„ค์ •

    • When [์‹คํ–‰] : ์‹œ๋‚˜๋ฆฌ์˜ค ์ง„ํ–‰ ํ•„์š”์กฐ๊ฑด ๋ช…์‹œ, ํ…Œ์ŠคํŠธํ•˜๊ณ ์ž ํ•˜๋Š” ํ–‰๋™

    • Then [๊ฒ€์ฆ] : ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์™„๋ฃŒํ–ˆ์„ ๋•Œ ๋ณด์žฅํ•ด์•ผ ํ•˜๋Š” ๊ฒฐ๊ณผ๋ฅผ ๋ช…์‹œ, ์˜ˆ์ƒ๋˜๋Š” ๋ณ€ํ™” ์„ค๋ช…

์˜ˆ์ œ๋ฅผ ํ†ตํ•ด given - when - then ํŒจํ„ด์œผ๋กœ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑ

// TextField.tsx

import React, { 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 = (event: React.ChangeEvent<HTMLInputElement>) => {
    const { value } = event.target;
    setText(value);
  };

  return (
    <div>
      <label htmlFor={id.current}>
        {label}
      </label>
      <input
        id={id.current}
        type="text"
        placeholder={placeholder}
        value={text}
        onChange={handleChange}
      />
    </div>
  );
}
// TextField.test.tsx
import { fireEvent, render, screen } from '@testing-library/react';

import TextField from './TextField';

const context = describe;
describe('TextField', () => {

  // 1. Given 
  // labe, text, setTest ํ•จ์ˆ˜,๋ Œ๋”๋ง๋˜์–ด์•ผ ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ ํ•จ์ˆ˜ ์ค€๋น„ 
  const label = '๊ฒ€์ƒ‰';
  const text = 'Search Text';
  const setText = jest.fn(); // โ†’ mocking

  beforeEach(() => {
    setText.mockClear();
    // ๋˜๋Š” jest.clearAllMocks();
  });

  function renderTextField() {
    render(
      <TextField
        label={label}
        placeholder="Input your name"
        text={text}
        setText={setText}
      />
    );
  }

  it('renders an input control', () => {
    // 2. When
    // TextField ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ™”๋ฉด์— ๋ Œ๋”๋ง ๋  ๋•Œ
    renderTextField();


    // 3.Then 
    // ํ™”๋ฉด Dom์—์„œ label, value, placeholder ์†์„ฑ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ์š”์†Œ๋ฅผ ์ฐพ๋Š”๋‹ค.
    screen.getByLabelText(label); 
    screen.getByDisplayValue(text);
    screen.getByPlaceholderText('์‹๋‹น์ด๋ฆ„');
  });

});

Mocking

  • ํ…Œ์ŠคํŠธํ•˜๊ณ ์ž ํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ ์˜์กดํ•˜๋Š” function์ด๋‚˜ class์— ๋Œ€ํ•ด ๋ชจ์กฐํ’ˆ์„ ๋งŒ๋“ค์–ด '์ผ๋‹จ' ๋Œ์•„๊ฐ€๊ฒŒ ํ•˜๋Š” ๊ฒƒ

โ‡’ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•  ๋•Œ, ํ•ด๋‹น ์ฝ”๋“œ๊ฐ€ ์˜์กดํ•˜๋Š” ๋ถ€๋ถ„์„ ๊ฐ€์งœ(mock)๋กœ ๋Œ€์ฒดํ•˜๋Š” ๊ธฐ๋ฒ•

๐Ÿค” ์™œ ๊ฐ€์งœ๋กœ ๋Œ€์ฒดํ•˜๋Š”๊ฐ€?

ํ…Œ์ŠคํŠธ ํ•˜๊ณ  ์‹ถ์€ ๊ธฐ๋Šฅ์ด ๋‹ค๋ฅธ ๊ธฐ๋Šฅ๋“ค๊ณผ ์—ฎ์–ด์žˆ์„ ๊ฒฝ์šฐ(์˜์กด) ์ •ํ™•ํ•œ ํ…Œ์ŠคํŠธ๊ฐ€ ํž˜๋“ค๊ธฐ ๋•Œ๋ฌธ์—

๐Ÿค” ๊ทธ๋Ÿผ Mocking์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ๋Š”?

  • ์™ธ๋ถ€ ์˜์กด์„ฑ์ด ํฐ ์ฝ”๋“œ(API ์š”์ฒญ ๋“ฑ)๋ฅผ ์ž‘์„ฑํ•  ๊ฒฝ์šฐ, ํ•ด๋‹น ๋ถ€๋ถ„๋งŒ ๊ฐ€์งœ๋กœ ๊ตฌํ˜„

๋งค๋ฒˆ ์„œ๋ฒ„๋ฅผ ๋„์šฐ๊ธฐ ์–ด๋ ต๊ณ , ์‹ค์„œ๋ฒ„๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์–ด๋ ค์šด ๋ฌธ์ œ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด์„œ ํ…Œ์ŠคํŠธ์—์„œ๋งŒ Mocking์„ ํ†ตํ•ด ์„œ๋ฒ„๋ฅผ ๊ตฌํ˜„ ๋ณดํ†ต ๋ฐฑ์—”๋“œ์™€ ์†Œํ†ตํ•˜๋Š” ๋ถ€๋ถ„์„ Mocking ํ•ด์„œ ํ…Œ์ŠคํŠธํ•ด์•ผ ํ•˜๋Š”๋ฐ, ์ด ๋ถ€๋ถ„์„ ํ•˜๋‚˜์”ฉ ๊ฐ€์งœ ๊ตฌํ˜„์œผ๋กœ ๋ฐ”๊พธ๋‹ค ๋ณด๋ฉด ์–ด๋ ค์šธ ๋•Œ๊ฐ€ ์žˆ๋‹ค. ์ด๋Ÿด ๋• MSW ๋“ฑ ๋‹ค๋ฅธ ๋Œ€์•ˆ์„ ๊ณ ๋ คํ•ด์•ผ ํ•œ๋‹ค.

Think in React ์˜ˆ์ œ๋ฅผ ํ†ตํ•ด API ์š”์ฒญ ์ฝ”๋“œ ๋ชจํ‚น

// App.tsx
import FilterableProductTable from './components/FilterableProductTable';
import useFetchProducts from './hooks/useFetchProducts';

export default function App() {
  // useFetchProducts : Custom hook์œผ๋กœ API ์‘๋‹ต๋ฐ›์€ ๊ฐ์ฒด products๋กœ ์ „๋‹ฌ 
  const products = useFetchProducts();

  return (
    <>
      <h1>์ƒํ’ˆ</h1>
      <FilterableProductTable products={products} />
    </>
  );
}
// App.test.tsx

import {render, screen} from '@testing-library/react';

import App from './App';

// const products  ===  mocking์„ ํ†ตํ•ด data ์ธ์‹
jest.mock('./hooks/useFetchProducts', () => () => [
    {
        category: 'Fruits', price: '$1', stocked: true, name: 'Apple',
    },
]);

test('App', () => {
    render(<App/>);
    screen.getByText('Apple'); // ํ™”๋ฉด์—์„œ Apple์ด๋ผ๋Š” ํ…์ŠคํŠธ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ํ…Œ์ŠคํŠธ
});

Test fixture

๐Ÿ“– Fixture๋Š” '๊ณ ์ •๋˜์–ด ์žˆ๋Š” ๋ฌผ์ฒด'๋ฅผ ์˜๋ฏธํ•œ๋‹ค.

๐Ÿค” Junit ํŒ€์—์„œ ๋งํ•˜๋Š” ํ…Œ์ŠคํŠธ ํ”ฝ์Šค์ฒ˜๋ž€?

  • ํ…Œ์ŠคํŠธ ์‹คํ–‰์„ ์œ„ํ•ด ๋ฒ ์ด์Šค๋ผ์ธ์œผ๋กœ์„œ ์‚ฌ์šฉ๋˜๋Š” ๊ฐ์ฒด๋“ค์˜ ๊ณ ์ •๋œ ์ƒํƒœ

โœ๐Ÿป ๋‚ด๊ฐ€ ์ƒ๊ฐํ•˜๋Š” ํ…Œ์ŠคํŠธ ํ”ฝ์Šค์ฒ˜๋Š”

  • mockingํ•œ ๋‚ด์šฉ๋“ค์„ ํ•œ๊ณณ์— ๋ชจ์•„๋‘๊ณ  ๋‹ค๋ฅธ๊ณณ์—์„œ ์žฌ์‚ฌ์šฉ ํ•˜๊ธฐ ์œ„ํ•œ ์šฉ๋„๋กœ ๋งŒ๋“œ๋Š” ํด๋”

๐Ÿ“ ํด๋” ๊ตฌ์กฐ

์ง์ ‘ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ

โ”œโ”€โ”€ src
โ”‚   โ”œโ”€โ”€ App.test.tsx
โ”‚   โ”œโ”€โ”€ App.tsx
โ”œโ”€โ”€ fixtures
โ”‚   โ”œโ”€โ”€ index.ts
โ”‚   โ””โ”€โ”€ products.ts
// App.test.tsx

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

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

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

    screen.getByText('Apple');
});
// fixtures/products.ts

const products = [
    {
        category: 'Fruits', price: '$1', stocked: true, name: 'Apple',
    },
];

export default products;
// fixtures/index.ts

import products from './products';

export default {
    products,
};

๋ณต์žกํ•ด์ง€๋ฉด ์ด ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉ โ†’ mocks ํด๋”๋ฅผ ๋ถ„๋ฆฌ

โ”‚   โ”œโ”€โ”€ hooks
โ”‚   โ”‚   โ”œโ”€โ”€ __mocks__
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ useFetchProducts.ts
โ”‚   โ”‚   โ””โ”€โ”€ useFetchProducts.ts
// App.test.tsx

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

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

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

    screen.getByText('Apple');
});
// hooks/__mocks__/useFetchProducts.ts

import fixtures from '../../../fixtures';

// const useFetchProducts = () => fixtures.products; // ์ด๋ ‡๊ฒŒ ์จ๋„ ๋˜์ง€๋งŒ 
const useFetchProducts = jest.fn(() => fixtures.products); // ๋ชจํ‚น์„ ๋“œ๋Ÿฌ๋‚ด๊ธฐ ์œ„ํ•ด ๊ถŒ์žฅ๋˜๋Š” ๋ฐฉ๋ฒ•  

export default useFetchProducts;

๐Ÿ”— ์ฐธ๊ณ 

Last updated