React TDD (Jest, enzyme)

์žญ์žญ์ดยท2021๋…„ 4์›” 14์ผ
2

์•Œ๋ฉด ์œ ์šฉํ•œ ์ง€์‹

๋ชฉ๋ก ๋ณด๊ธฐ
2/5
post-thumbnail

React TDD (Jest, enzyme)

๐ŸŽ ๋ชฉ์ฐจ

0. ๊ฐœ์š”

  • Jest: Test Framework
  • enzyyme: Test library

1. ์Šค๋ƒ…์ƒท ํ…Œ์ŠคํŒ…

// ํ…Œ์ŠคํŠธ ์ƒ˜ํ”Œ์ฝ”๋“œ
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';

window.matchMedia = window.matchMedia || function() {
  return {
      matches : false,
      addListener : function() {},
      removeListener: function() {}
  };
};

describe('<App/>', () => {
  it('matches snapshot', () => {
    const utils = render(<App/>);
    expect(utils.container).toMatchSnapshot();
  })
  it('shows the props correctly', () => {
    const utils = render(<App/>);
    utils.getByText('ISSUER');
  })
})
  • describe: ํ•˜๋‚˜์˜ ํ…Œ์ŠคํŠธ ๊ทธ๋ฃน

  • it: ๋‹จ์œ„ ํ…Œ์ŠคํŠธ

  • ์œ„ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰ ์‹œ src/snapshots ์•ˆ์— App.test.js.snap ์ƒ์„ฑ

    ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ Œ๋”๋ง๋์„ ๋•Œ ์Šค๋ƒ…์ƒท๊ณผ ๋ถˆ์ผ์น˜ํ•˜๋ฉด ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํŒจ

    ์Šค๋ƒ…์ƒท ์—…๋ฐ์ดํŠธ๋Š” ์‹คํ–‰ ์ฝ˜์†”์—์„œ uํ‚ค

// Jest + enzyme
import React from 'react';
import Counter from './counter.jsx';
import { shallow, configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });

describe('<Counter />', () => {
  it('์„ฑ๊ณต์ ์œผ๋กœ ๋ Œ๋”๋ง๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.', () => {
    const wrapper = shallow(<Counter />);
    expect(wrapper.length).toBe(1);
  });

  it('ํƒ€์ดํ‹€ ์ธํ’‹์ด ๋ Œ๋”๋ง๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.', () => {
    const wrapper = shallow(<Counter />);
    expect(wrapper.find('#title').length).toEqual(1);
  });

  it('ํƒ€์ดํ‹€์ด ๋ณ€๊ฒฝ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.', () => {
    const wrapper = shallow(<Counter />);
    wrapper.find('#title').simulate('change', { target: { value: '๊ฐ’' } });
    expect(wrapper.state().title).toBe('๊ฐ’');
  });

  it('์ˆซ์ž๊ฐ€ ์˜ฌ๋ผ๊ฐ€์•ผ ํ•ฉ๋‹ˆ๋‹ค.', () => {
    const wrapper = shallow(<Counter />);
    wrapper.find('#up').simulate('click');
    wrapper.find('#up').simulate('click');
    expect(wrapper.state().value).toBeLessThan(1);
  });
});

2. ์ฟผ๋ฆฌ

  • Variant
    • getBy: ๋‹จ์ผ์„ ํƒ
    • getAllBy: ๋ณต์ˆ˜์„ ํƒ
    • queryBy: geyBy์™€ ๊ฐ™์Œ. ๋‹จ ์กด์žฌํ•˜์ง€ ์•Š์•„๋„ ์—๋Ÿฌ ์•ˆ๋‚จ
    • queryAllBy: getAllBy์™€ ๊ฐ™์Œ. ๋‹จ ์กด์žฌํ•˜์ง€ ์•Š์•„๋„ ์—๋Ÿฌ ์•ˆ๋‚จ
    • findBy: Promise of getBy
    • findAllBy: Promise of getAllBy
  • Queries
    • ByLabelText: label ๊ฐ’์œผ๋กœ input ์„ ํƒ
<label for="username-input">์•„์ด๋””</label>
<input id="username-input" />

const inputNode = getByLabelText('์•„์ด๋””');
  • ByPlaceholderText: placeholder ๊ฐ’์œผ๋กœ input/textarea ์„ ํƒ
<input placeholder="์•„์ด๋””" />;

const inputNode = getByPlaceholderText('์•„์ด๋””');
  • ByText: text ๊ฐ’์œผ๋กœ DOM์„ ํƒ
<div>Hello World!</div>;

const div = getByText('Hello World!');
  • ByAltText: alt ์†์„ฑ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” element ์„ ํƒ(์ฃผ๋กœ img)
<img src="/awesome.png" alt="awesome image" />;

const imgAwesome = getByAltText('awesomse image');
  • ByTitle: title ์†์„ฑ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” DOM
<p>
<span title="React">๋ฆฌ์•กํŠธ</span>๋Š” ์งฑ ๋ฉ‹์ง„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‹ค.
</p>

<svg>
  <title>Delete</title>
  <g><path/></g>
</svg>

const spanReact = getByTitle('React');
const svgDelete = getByTitle('Delete');
  • ByDisplayValue: input/textarea/select๊ฐ€ ์ง€๋‹ˆ๊ณ  ์žˆ๋Š” ํ˜„์žฌ ๊ฐ’
<input value="text" />;

const input = getByDisplayValue('text');
  • ByRole: ํŠน์ • role ๊ฐ’์„ ์ง€๋‹ˆ๋Š” element
<span role="button">์‚ญ์ œ</span>;

const spanRemove = getByRole('button');
  • ByTestId: customize
<div data-testid="commondiv">ํ”ํ•œ div</div>;

const commonDiv = getByTestId('commondiv');
  • ์šฐ์„ ์ˆœ์œ„
1. getByLabelText
2. getByPlaceholderText
3. getByText
4. getByDisplayValue
5. getByAltText
6. getByTitle
7. getByRole
8. getByTestId

3. ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ

3.1. FireEvent

fireEvent.์ด๋ฒคํŠธ์ด๋ฆ„(DOM, ์ด๋ฒคํŠธ๊ฐ์ฒด);

fireEvent.change(myInput, { target: { value: 'hello world' } });

3.2. Component

// src/Counter.js
import React, { useState, useCallback } from 'react';

const Counter = () => {
  const [number, setNumber] = useState(0);

  const onIncrease = useCallback(() => {
    setNumber(number + 1);
  }, [number]);

  const onDecrease = useCallback(() => {
    setNumber(number - 1);
  }, [number]);

  return (
    <div>
      <h2>{number}</h2>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
    </div>
  );
};

3.3. Component test

// src/Counter.test.js
import React from 'react';
import { render, fireEvent } from 'react-testing-library';
import Counter from './Counter';

describe('<Counter />', () => {
  it('matches snapshot', () => {
    const utils = render(<Counter />);
    expect(utils.container).toMatchSnapshot();
  });
  it('has a number and two buttons', () => {
    const utils = render(<Counter />);
    // ๋ฒ„ํŠผ๊ณผ ์ˆซ์ž๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ
    utils.getByText('0');
    utils.getByText('+1');
    utils.getByText('-1');
  });
  it('increases', () => {
    const utils = render(<Counter />);
    const number = utils.getByText('0');
    const plusButton = utils.getByText('+1');
    // ํด๋ฆญ ์ด๋ฒคํŠธ๋ฅผ ๋‘๋ฒˆ ๋ฐœ์ƒ์‹œํ‚ค๊ธฐ
    fireEvent.click(plusButton);
    fireEvent.click(plusButton);
    expect(number).toHaveTextContent('2'); // jest-dom ์˜ ํ™•์žฅ matcher ์‚ฌ์šฉ
    expect(number.textContent).toBe('2'); // textContent ๋ฅผ ์ง์ ‘ ๋น„๊ต
  });
  it('decreases', () => {
    const utils = render(<Counter />);
    const number = utils.getByText('0');
    const plusButton = utils.getByText('-1');
    // ํด๋ฆญ ์ด๋ฒคํŠธ๋ฅผ ๋‘๋ฒˆ ๋ฐœ์ƒ์‹œํ‚ค๊ธฐ
    fireEvent.click(plusButton);
    fireEvent.click(plusButton);
    expect(number).toHaveTextContent('-2'); // jest-dom ์˜ ํ™•์žฅ matcher ์‚ฌ์šฉ
  });
});

0๊ฐœ์˜ ๋Œ“๊ธ€