- javascript์์ ํ ์คํ ๋๊ตฌ์ ์ ๋ง ์ฌ๋ฌ๊ฐ์ง๊ฐ ์๋ค. ์ฌ๊ธฐ์๋ jest๋ฅผ ์ฌ์ฉํด ๋ณผ ๊ฒ์ด๋ค.
// sum.js
function sum(a, b) {
return a + b;
}
function sumOf(numbers) {
// let result = 0;
// numbers.forEach(n => {
// result += n;
// });
// return result;
// reduce๋ฅผ ์ด์ฉํ์ฌ ๋ฐฐ์ด์ ์ดํฉ์ ๊ตฌํ๋ค.
return numbers.reduce((acc, current) => acc + current, 0);
}
// ๊ฐ๊ฐ ๋ด๋ณด๋ด๊ธฐ
exports.sum = sum;
exports.sumOf = sumOf;// sum.test.js
describe('sum', () => {
it('calculates 1 + 2', () => {
expect(sum(1, 2)).toBe(3);
});
it('calculates all numbers', () => {
const array = [1,2,3,4,5];
expect(sumOf(array)).toBe(15);
});
});- ํ ์คํธ ์ผ์ด์ค๋ฅผ ๋ง๋ค๋ ์ฌ์ฉํ๋ ํจ์
- ์ฌ๋ฌ๊ฐ์ ํ ์คํธ ์ผ์ด์ค๋ฅผ ๋ฌถ์ด ์ค๋ ์ฌ์ฉํ๋ ํจ์
- ํน์ ๊ฐ์ด ~~์ผ ๊ฒ์ด๋ค ๋ผ๊ณ ์ฌ์ ์ ์ ์ํ๊ณ ํต๊ณผํ๋ฉด ํ ์คํธ๋ฅผ ์ฑ๊ณต์ํค๊ณ ํต๊ณผํ์ง ์์ผ๋ฉด ํ ์คํธ๋ฅผ ์คํจ์ํต๋๋ค.
- ํน์ ๊ฐ๊ณผ ์ฐ๋ฆฌ๊ฐ ์ ํ ๊ฐ๊ณผ ์ผ์นํ๋์ง ํ์ธ์ ํด์ค๋๋ค.
- ๋ฆฌํฉํ ๋ง ์ดํ ์ฝ๋๊ฐ ์ ๋๋ก ์๋ํ๊ณ ์๋๊ฒ์ ๊ฒ์ฆํ๊ธฐ ๋งค์ฐ ๊ฐํธํ๋ค.
- react-dom/test-utils ์์ ๋ค์ด ์๋ ์ ํธ ํจ์๋ฅผ ์ฌ์ฉํ์ฌ๋ ํ ์คํธ๋ฅผ ์งํํ ์ ์์ง๋ง ๋ณต์กํ๊ณ ๋ถํธํ ๋ถ๋ถ๋ค์ด ์์ด์ React ๊ณต์ ๋ฌธ์์์๋ ํ ์คํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ ๊ถ์ฅํ๊ณ ์๋ค.
- React ๊ณต์ ๋ฌธ์์์ ๊ถ์ฅํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ react-testing-library ์ด๋ค. ํ์ฌ ๋ง์ด ์ฌ์ฉ๋๋ Enzyme์ ๋์ฒด ๋ฐฉ์์ด๋ผ๊ณ ์ธ๊ธํ๊ณ ์๋ค.
- Enzyme์ Airbnb์์ ๋ง๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ก ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ ๋ ์ปดํฌ๋ํธ ๋ด๋ถ ๊ธฐ๋ฅ์ ์์ฃผ ์ ๊ทผํ๋ค. ์๋ฅผ ๋ค์ด ์ปดํฌ๋ํธ๊ฐ ๊ฐ์ง๊ณ ์๋ props, state๋ฅผ ํ์ธํ๊ณ ์ปดํฌ๋ํธ์ ๋ด์ฅ ๋ฉ์๋๋ฅผ ์ง์ ํธ์ถํ๊ธฐ๋ ํ๋ค.
- react-testing-library๋ ๋ ๋๋ง ๊ฒฐ๊ณผ์ ์ง์คํ๋ค. ์ปดํฌ๋ํธ์ ์ธ์คํด์ค์ ๋ํด์ ์ ๊ฒฝ์ฐ์ง ์๊ณ ์ค์ DOM์ ๋ํ์ฌ ์ ๊ฒฝ์ ๋ง์ด ์ฐ๊ณ , ์ค์ ํ๋ฉด์ ๋ฌด์์ด ๋ณด์ฌ์ง๋์ง, ์ด๋ ํ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ์๋ ํ๋ฉด์ ์ํ๋ ๋ณํ๊ฐ ์๊ฒผ๋์ง ์ด๋ฐ ๊ฒ์ ํ์ธํ๊ธฐ์ ์กฐ๊ธ ๋ ์ต์ ํ ๋์ด์์ต๋๋ค. ๊ทธ๋์ ์ฌ์ฉ์ ๊ด์ ์์ ํ ์คํธํ๋๊ฒ์ด ๋ ์ฉ์ดํฉ๋๋ค.
- 2๊ฐ๋ฅผ ์ ์ ํ ์ฌ์ฉํ๋ฉด ๋๋ค.
$ npm i enzyme enzyme-adapter-react-16import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({ adapter: new Adapter() });- ์ค๋ ์ท ํ ์คํ ์ด๋, ๋ ๋๋ง๋ ๊ฒฐ๊ณผ๊ฐ ์ด์ ์ ๋ ๋๋งํ ๊ฒฐ๊ณผ์ ์ผ์นํ๋ ํ์ธํ๋ ์์
$ npm i enzyme-to-json
``
##### ์ค๋
์ท ์ค์
โ```javascript
// package.json ์ ์๋ ๋ด์ฉ ์ถ๊ฐ
"jest": {
"snapshotSerializers": ["enzyme-to-json/serializer"]
}import React from "react";
import { mount } from "enzyme";
import Profile from './Profile';
describe('<Profile />', () => {
it('matches snapshot', () => {
// mountํจ์๋ Enzyme ์ ํตํ์ฌ ๋ฆฌ์กํธ ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋ง ํด์ค๋๋ค.
const wrapper = mount(<Profile username="tintoll" name="ํ๋" />);
expect(wrapper).toMatchSnapshot();
});
it('renders username and name', () => {
const wrapper = mount(<Profile username="tintoll" name="ํ๋" />);
expect(wrapper.props().username).toBe('tintoll');
expect(wrapper.props().name).toBe('ํ๋');
// find ํจ์๋ฅผ ์ฌ์ฉํ๋ฉด ํน์ DOM์ ์ ํํ ์ ์๋ค. querySelector์ ๋๊ฐ์ ๋ฐฉ์์ผ๋ก ์ฌ์ฉํ ์ ์๋ค.
const boldElement = wrapper.find('b');
expect(boldElement.contains('tintoll')).toBe(true);
const spanElement = wrapper.find('span');
expect(spanElement.text()).toBe('(ํ๋)');
})
});import React from 'react';
import { shallow } from 'enzyme';
import Counter from './Counter';
describe('<Counter />', () => {
it('matches snapshot', () => {
// shallow๋ ์ปดํฌ๋ํธ ๋ด๋ถ์ ๋ ๋ค๋ฅธ ๋ฆฌ์กํธ ์ปดํฌ๋ํธ๊ฐ ์๋ค๋ฉด ์ด๋ฅผ ๋ ๋๋งํ์ง ์์ต๋๋ค.
// mount๋ ์ปดํฌ๋ํธ ๋ด๋ถ ์ปดํฌ๋ํธ๊น์ง ๋ ๋๋ง ๋๋ค.
const wrapper = shallow(<Counter />);
expect(wrapper).toMatchSnapshot();
});
it('has initial number', () => {
const wrapper = shallow(<Counter />);
// ์ปดํฌ๋ํธ state๋ฅผ ์กฐํํ ๋ state()ํจ์๋ฅผ ์ด์ฉ
expect(wrapper.state().number).toBe(0);
});
it('increases', () => {
const wrapper = shallow(<Counter />);
// ๋ด์ฅ ๋ฉ์๋๋ฅผ ํธ์ถํ ๋ instance()ํจ์๋ฅผ ํธ์ถํ์ฌ ์ธ์คํด์ค๋ฅผ ์กฐํํ ๋ฉ์๋ ํธ์ถ
wrapper.instance().handleIncrease();
expect(wrapper.state().number).toBe(1);
});
it('decreases', () => {
const wrapper = shallow(<Counter />);
wrapper.instance().handleDecrease();
expect(wrapper.state().number).toBe(-1);
});
it('calls handleIncrease', () => {
// ํด๋ฆญ์ด๋ฒคํธ๋ฅผ ์๋ฎฌ๋ ์ดํธ ํ๊ณ state๋ฅผ ํ์ธ
const wrapper = shallow(<Counter />);
const plusButton = wrapper.findWhere(
node => node.type() === 'button' && node.text() === '+1'
);
plusButton.simulate('click');
expect(wrapper.state().number).toBe(1);
});
it('calls handleDecrease', () => {
// ํด๋ฆญ์ด๋ฒคํธ๋ฅผ ์๋ฎฌ๋ ์ดํธ ํ๊ณ h2ํ์ ํ
์ค๋ฅผ ํ์ธ
const wrapper = shallow(<Counter />);
// findWhere ํจ์๋ฅผ ์ฌ์ฉํ๋ฉด ์ํ๋ ์กฐ๊ฑด์ ๋ง์กฑํ๋ ํ๊ทธ๋ฅผ ์ ํํ ์ ์๋ค.
const minuxButton = wrapper.findWhere(
node => node.type() === 'button' && node.text() === '-1'
);
// ์ด๋ฒคํธ๋ฅผ ์๋ฎฌ๋ ์ดํธ ํ ๋์๋ ์ํ๋ ์๋ฆฌ๋จผํธ๋ฅผ ์ฐพ์์ simulate()ํจ์๋ฅผ ์ฌ์ฉํฉ๋๋ค.
// ์ฒซ๋ฒ์งธ๋ ์ด๋ฒคํธ์ด๋ฆ, ๋๋ฒ์งธ๋ ์ด๋ฒคํธ ๊ฐ์ฒด๋ฅผ ๋ฃ์ต๋๋ค.
/*
// input์ change์ด๋ฒคํธ์ผ ๊ฒฝ์ฐ
input.simulate('change', {
target: {
value: 'hello world'
}
});
*/
minuxButton.simulate('click');
const number = wrapper.find('h2');
expect(number.text()).toBe('-1');
});
});import React from 'react';
import { mount } from 'enzyme';
import HookCounter from './HookCounter';
// ํจ์ํ ์ปดํฌ๋ํธ์์๋ ํด๋์คํ ์ปดํฌ๋ํธ์ ๋ฌ๋ฆฌ ์ธ์คํด์ค ๋ฉ์๋ ๋ฐ ์ํ๋ฅผ ์กฐํ ํ ๋ฐฉ๋ฒ์ด ์์ต๋๋ค.
// Hooks ๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ๊ผญ shallow ๊ฐ ์๋ mount ๋ฅผ ์ฌ์ฉํ์
์ผ ํฉ๋๋ค.
// ๊ทธ ์ด์ ๋, useEffect Hook ์ shallow ์์ ์๋ํ์ง ์๊ณ , ๋ฒํผ ์๋ฆฌ๋จผํธ์ ์ฐ๊ฒฐ๋์ด์๋ ํจ์๊ฐ ์ด์ ํจ์๋ฅผ ๊ฐ๋ฅดํค๊ณ ์๊ธฐ ๋๋ฌธ์,
// ์๋ฅผ ๋ค์ด +1 ๋ฒํผ์ ํด๋ฆญ ์ด๋ฒคํธ๋ฅผ ๋๋ฒ ์๋ฎฌ๋ ์ดํธํด๋ ๊ฒฐ๊ณผ๊ฐ์ด 2๊ฐ ๋๋๊ฒ ์๋๋ผ 1์ด ๋ฉ๋๋ค.
describe('<HookCounter />', () => {
it('matches snapshot', () => {
const wrapper = mount(<HookCounter />);
expect(wrapper).toMatchSnapshot();
});
it('increases', () => {
const wrapper = mount(<HookCounter />);
let plusButton = wrapper.findWhere(
node => node.type() === 'button' && node.text() === '+1'
);
plusButton.simulate('click');
plusButton.simulate('click');
const number = wrapper.find('h2');
expect(number.text()).toBe('2');
});
it('decreases', () => {
const wrapper = mount(<HookCounter />);
let decreaseButton = wrapper.findWhere(
node => node.type() === 'button' && node.text() === '-1'
);
decreaseButton.simulate('click');
decreaseButton.simulate('click');
const number = wrapper.find('h2');
expect(number.text()).toBe('-2');
});
});# ์์ ๋ฒ์
$ npm i react-testing-library jest-dom
# ์ต์ ๋ฒ์
$ npm i @testing-library/react @testing-library/jest-dom// ๋ฆฌ์กํธ์์ DOM ์๋ฎฌ๋ ์ด์
์ ์ํ [JSDOM]์ด๋ผ๋ ๋๊ตฌ๋ฅผ ์ฌ์ฉํ์ฌ document.body์ ๋ฆฌ์กํธ ์ปดํฌ๋ํธ๋ฅผ ๋ ๋๋งํ๋ค.
// cleanup-after-each๋ฅผ ๋ถ๋ฌ์ค๋ฉด ๊ฐ ํ
์คํธ ์ผ์ด์ค๊ฐ ๋๋ ๋๋ง๋ค ๊ธฐ์กด์ ๊ฐ์์ ํ๋ฉด์ ๋จ์์๋ UI๋ฅผ ์ ๋ฆฌํฉ๋๋ค.
import '@testing-library/react/cleanup-after-each';
// jest์์ DOM๊ด๋ จ matcher๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ ํด์ค๋ค.
import '@testing-library/jest-dom/extend-expect';import React from 'react';
import { render } from "@testing-library/react";
import Profile from './Profile';
describe('<Profile />', () => {
it('matches snapshot', () => {
const utils = render(<Profile username="velopert" name="๊น๋ฏผ์ค" />);
expect(utils.container).toMatchSnapshot();
});
it('shows the props correctly', () => {
const utils = render(<Profile username="velopert" name="๊น๋ฏผ์ค" />);
utils.getByText('velopert!'); // velopert๋ผ๋ ํ
์คํธ๋ฅผ ๊ฐ์ง ์๋ฆฌ๋จผํธ๊ฐ ์๋์ง ํ์ธ
utils.getByText('(๊น๋ฏผ์ค)');// (๊น๋ฏผ์ค)๋ผ๋ ํ
์คํธ๋ฅผ ๊ฐ์ง ์๋ฆฌ๋จผํธ๊ฐ ์๋์ง ํ์ธ
utils.getByText(/๊น/); // ์ ๊ท์ /๊น/ ์ ํต๊ณผํ๋ ์๋ฆฌ๋จผํธ๊ฐ ์๋์ง ํ์ธ
});
});render ํจ์๋ฅผ ์คํํ๊ณ ๋๋ฉด ๊ทธ ๊ฒฐ๊ณผ๋ฌผ ์์๋ ๋ค์ํ ์ฟผ๋ฆฌ ํจ์๋ค์ด ์๋ค. ์ด ์ฟผ๋ฆฌ ํจ์๋ค์ Variant ์ Queries ์ ์กฐํฉ์ผ๋ก ๋ค์ด๋ฐ์ด ์ด๋ฃจ์ ธ ์๋ค.
๋๋ฌด ๋ง์์ ์ถ์ฒํ๋ ์ฟผ๋ฆฌ๋ง ์ฌ์ฉํ๋ฉด ๋ ๋ฏ ํ๋ค.
-
getByLabelText
-
label ์ด ์๋ input ์ label ๋ด์ฉ์ผ๋ก input ์ ์ ํํฉ๋๋ค.
<label for="username-input">์์ด๋</label> <input id="username-input" /> const inputNode = getByLabelText('์์ด๋');
-
-
getByPlaceholderText
-
placeholder ๊ฐ์ผ๋ก input ๋ฐ textarea ๋ฅผ ์ ํํฉ๋๋ค.
<input placeholder="์์ด๋" />; const inputNode = getByPlaceholderText('์์ด๋');
-
-
getByText
-
์๋ฆฌ๋จผํธ๊ฐ ๊ฐ์ง๊ณ ์๋ ํ ์คํธ ๊ฐ์ผ๋ก DOM ์ ์ ํํฉ๋๋ค.
<div>Hello World!</div>; const div = getByText('Hello World!');
-
-
getByDisplayValue
-
input,textarea,select๊ฐ ์ง๋๊ณ ์๋ ํ์ฌ ๊ฐ์ ๊ฐ์ง๊ณ ์๋ฆฌ๋จผํธ๋ฅผ ์ ํํฉ๋๋ค.<input value="text" />; const input = getByDisplayValue('text');
-
-
getByAltText
-
alt์์ฑ์ ๊ฐ์ง๊ณ ์๋ ์๋ฆฌ๋จผํธ (์ฃผ๋กimg) ๋ฅผ ์ ํํฉ๋๋ค.<img src="/awesome.png" alt="awesome image" />; const imgAwesome = getByAltText('awesomse image');
-
-
getByTitle
title์์ฑ์ ๊ฐ์ง๊ณ ์๋ DOM ํน์title์๋ฆฌ๋จผํธ๋ฅผ ์ง๋๊ณ ์๋ SVG ๋ฅผ ์ ํ ํ ๋ ์ฌ์ฉํฉ๋๋ค.
-
getByRole
- ํน์
role๊ฐ์ ์ง๋๊ณ ์๋ ์๋ฆฌ๋จผํธ๋ฅผ ์ ํํฉ๋๋ค.
<span role="button">์ญ์ </span>; const spanRemove = getByRole('button');
- ํน์
-
getByTestId
-
๋ค๋ฅธ ๋ฐฉ๋ฒ์ผ๋ก ๋ชป ์ ํํ ๋ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ธ๋ฐ์, ํน์ DOM ์ ์ง์ test ํ ๋ ์ฌ์ฉํ id ๋ฅผ ๋ฌ์์ ์ ํํ๋ ๊ฒ์ ์๋ฏธํฉ๋๋ค.
<div data-testid="commondiv">ํํ div</div>; const commonDiv = getByTestId('commondiv');
-
fireEvent()ํจ์๋ ์ด๋ฒคํธ๋ฅผ ๋ฐ์์์ผ์ค๋๋ค. ์ฌ์ฉ๋ฒ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
fireEvent.์ด๋ฒคํธ์ด๋ฆ(DOM, ์ด๋ฒคํธ๊ฐ์ฒด);
fireEvent.change(myInput, { target: { value: 'hello world' } });jest.fn()ํจ์๋ jest์์ ์ ๊ณตํ๋ mockํจ์์ด๋ค. ์ด ํจ์๋ฅผ ์ฌ์ฉํ๋ฉด ์ด ํจ์๊ฐ ํธ์ถ๋ ๋ค์ toBeCalled, toBeCalledWith์ ๊ฐ์ matcher๋ฅผ ์ฌ์ฉํ์ฌ ํจ์๊ฐ ํธ์ถ๋๋์ง, ํธ์ถ๋์ผ๋ฉด ์ด๋ค ํ๋ผ๋ฏธํฐ๋ก ํธ์ถ๋๋์ง ์ฝ๊ฒ ํ์ธ ํ ์ ์์ต๋๋ค.
it('calls onInsert and clears input', () => {
const onInsert = jest.fn();
const { input, button } = setup({ onInsert });
// ํ
์คํธ ์ฝ๋๋ฅผ ์์ฑ ํ ๋์๋ ๋ง์น ์ฌ์ฉ์๊ฐ ๋ ์
์ฅ์ผ๋ก,์๋์ ๊ฐ์ ํ๋ฆ์ผ๋ก ์์ฑํ๋ฉด ๋๋ค.
// ๊ธ์์
๋ ฅํ๊ณ
fireEvent.change(input, {
target: {
value: 'TDD ๋ฐฐ์ฐ๊ธฐ'
}
});
// ๋ฑ๋ก ๋ฒํผํด๋ฆญ
fireEvent.click(button);
expect(onInsert).toBeCalledWith('TDD ๋ฐฐ์ฐ๊ธฐ'); // onInsert ๊ฐ 'TDD ๋ฐฐ์ฐ๊ธฐ' ํ๋ผ๋ฏธํฐ๊ฐ ํธ์ถ๋์ด์ผํจ
expect(input).toHaveAttribute('value',''); // input์ด ๋น์์ ธ์ผํจ.
})- toHaveStyle ์ด๋ผ๋ matcher ํจ์๋ฅผ ์ฌ์ฉํ๋ฉด ํด๋น DOM ์ ํน์ ์คํ์ผ์ด ์๋์ง ์ฝ๊ฒ ํ์ธ ํ ์ ์์ต๋๋ค.
- not ์ด๋ผ๋ ํค์๋๋ ํน์ ์กฐ๊ฑด์ด ๋ง์กฑํ์ง ์์์ผ ํจ์ ์๋ฏธ
it('shows line-through on span when done is false', () => {
const { span } = setup({ todo: { ...sampleTodo, done: false } });
expect(span).not.toHaveStyle('text-decoration: line-through;');
});- toBeInTheDocument ์ด๋ผ๋ matcher ํจ์๋ฅผ ์ฌ์ฉํ๋ฉด ํน์ ์๋ฆฌ๋จผํธ๊ฐ ํ๋ฉด์์ ์ฌ๋ผ์ก๋์ง ํ์ธํ ์ ์์ต๋๋ค.
expect(todoText).not.toBeInTheDocument();
// ๋ค๋ฅธ toBeInTheDocumentํจ์๋ฅผ ์ฌ์ฉํ์ง ์๊ณ ํํํ๊ธฐ
const removedText = queryByText('TDD ๋ฐฐ์ฐ๊ธฐ');
expect(removedText).toBeNull();- ๋น๋๊ธฐ ํ ์คํธ๋ฅผ ํ๊ธฐ ์ํด์๋ react-testing-library์์ ์ง์ํ๋ Async Utilitiesํจ์๋ค์ ์ฌ์ฉํ๋ฉด ๋๋ค.
- ํน์ ์ฝ๋ฐฑ์์ ์๋ฌ๋ฅผ ๋ฐ์ํ์ง ์์ ๋ ๊น์ง ๋๊ธฐํ๋ ํจ์
it('reveals text wheen toggle is ON', async () => {
const { getByText } = render(<DelayedToggle />);
const toggleButton = getByText('ํ ๊ธ');
fireEvent.click(toggleButton);
// ์ฝ๋ฐฑ ์์ ํจ์๊ฐ ์๋ฌ๊ฐ ๋ฐ์์ํค์ง ์์๋ ๊น์ง ๊ธฐ๋ค๋ฆฝ๋๋ค.
// timeout ๊ธฐ๋ณธ๊ฐ์ 4500ms ์ด๋ค.
await wait( () => getByText('์ผํธ!!'), {timeout : 3000});
});- ํน์ ์๋ฆฌ๋จผํธ๊ฐ, ๋ํ๋ฌ๊ฑฐ๋, ๋ฐ๋์๊ฑฐ๋, ์ฌ๋ผ์ง๋๊น์ง ๋๊ธฐ๋ฅผ ํด์ค๋๋ค. ๊ทธ๋ฆฌ๊ณ ํ๋ก๋ฏธ์ค๊ฐ ๋๋ ๋ ์ฐ๋ฆฌ๊ฐ ์ ํํ ์๋ฆฌ๋จผํธ๋ฅผ resolve ํฉ๋๋ค.
it('toggles text ON/OFF', async () => {
const { getByText } = render(<DelayedToggle />);
const toggleButton = getByText('ํ ๊ธ');
fireEvent.click(toggleButton);
// waitForElement ํจ์๋ ํน์ ์๋ฆฌ๋จผํธ๊ฐ, ๋ํ๋ฌ๊ฑฐ๋, ๋ฐ๋์๊ฑฐ๋, ์ฌ๋ผ์ง๋๊น์ง ๋๊ธฐ๋ฅผ ํด์ค๋๋ค.
// ํ๋ก๋ฏธ์ค๊ฐ ๋๋ ๋ ์ฐ๋ฆฌ๊ฐ ์ ํํ ์๋ฆฌ๋จผํธ๋ฅผ resolveํ๋ค.
const text = await waitForElement(() => getByText('ON'));
expect(text).toHaveTextContent('ON');
});- ์ฝ๋ฐฑํจ์๊ฐ ์๋๋ผ ๊ฒ์ฌํ๊ณ ์ถ์ ์๋ฆฌ๋จผํธ๋ฅผ ๋ฃ์ด์ฃผ๋ฉด ํด๋น ์๋ฆฌ๋จผํธ์์ ๋ณํ๊ฐ ๋ฐ์ ํ ๋ ๊น์ง ๊ธฐ๋ค๋ฆฝ๋๋ค. ํ๋ก๋ฏธ์ค๊ฐ resolve ๋์๋ mutationList๋ฅผ ๋ฐํํ์ฌ DOM์ด ์ด๋ป๊ฒ ๋ฐ๋์๋์ง์ ๋ํ ์ ๋ณด๋ฅผ ์์ ์๋ค.
it('changes something when button is clicked', async () => {
const { getByText, container } = render(<DelayedToggle />);
const toggleButton = getByText('ํ ๊ธ');
fireEvent.click(toggleButton);
// waitForDomChange๋ ์ฝ๋ฐฑํจ์๊ฐ ์๋๋ผ ๊ฒ์ฌํ๊ณ ์ถ์ ์๋ฆฌ๋จผํธ๋ฅผ ๋ฃ์ด์ฃผ๋ฉด ํด๋น ์๋ฆฌ๋จผํธ์์ ๋ณํ๊ฐ ๋ฐ์ํ ๋๊น์ง ๊ธฐ๋ค๋ ค์ค๋๋ค.
const mutations = await waitForDomChange({container});
// ํ๋ก๋ฏธ์ค๊ฐ resolve๋์๋ mutationList๋ฅผ ๋ฐํํ์ฌ DOM์ด ์ด๋ป๊ฒ ๋ฐ๋์๋์ง์ ๋ํ ์ ๋ณด๋ฅผ ์์ ์๋ค.
// console.log(mutations);
});- ํน์ ์๋ฆฌ๋จผํธ๊ฐ ํ๋ฉด์์ ์ฌ๋ผ์ง๋๊น์ง ๊ธฐ๋ค๋ฆฌ๋ ํจ์์ ๋๋ค.
t('removes text when toggle is OFF', async () => {
const { getByText, container } = render(<DelayedToggle />);
const toggleButton = getByText('ํ ๊ธ');
fireEvent.click(toggleButton);
await waitForDomChange({container}); // ON์ด ๋จ
getByText('์ผํธ!!');
fireEvent.click(toggleButton);
// waitForElementToBeRemove๋ ํน์ ์๋ฆฌ๋จผํธ๊ฐ ํ๋ฉด์์ ์ฌ๋ผ์ง๋๊น์ง ๊ธฐ๋ค๋ฆฌ๋ ํจ์์
๋๋ค.
await waitForElementToBeRemoved( () => getByText('์ผํธ!!'));
});- Rest API๋ฅผ ํธ์ถํด์ผํ๋ ๊ฒฝ์ฐ์๋ API๋ฅผ ์ง์ ํธ์ถํ์ง ์๊ณ ์ด๋ฅผ mocking ํฉ๋๋ค.
- MockAdapter ๋ฅผ ์ฌ์ฉํ๋ฉด ํน์ API ์์ฒญ์ด ๋ฐ์ํ์ ๋ ์ด๋ค ์๋ต์ด ์์ผ ํ๋์ง ์ง์ ์ ์ํด์ค ์ ์์ต๋๋ค.
- delayResponse ์ต์ ์ ์ค์ ํ๋ฉด ๋๋ ์ด๋ฅผ ์์์ ์ผ๋ก ์ค์ ํ ์ ์์ต๋๋ค. ์ด ์ค์ ์ ์์ด๋ ์๊ด ์์ต๋๋ค.
describe('<UserProfile />', () => {
const mock = new MockAdapter(axios, { delayResponse : 200}) // 200ms ๊ฐ์ง ๋๋ ์ด ์ค์
// API ์์ฒญ์ ๋ํ์ฌ ์๋ต ๋ฏธ๋ฆฌ ์ ํ๊ธฐ
mock.onGet('https://jsonplaceholder.typicode.com/users/1').reply(200, {
id: 1,
name: 'Leanne Graham',
username: 'Bret',
email: 'Sincere@april.biz',
address: {
street: 'Kulas Light',
suite: 'Apt. 556',
city: 'Gwenborough',
zipcode: '92998-3874',
geo: {
lat: '-37.3159',
lng: '81.1496'
}
},
phone: '1-770-736-8031 x56442',
website: 'hildegard.org',
company: {
name: 'Romaguera-Crona',
catchPhrase: 'Multi-layered client-server neural-net',
bs: 'harness real-time e-markets'
}
});
it('calls getUser API loads userData properly', async () => {
const { getByText } = render(<UserProfile id={1} />);
await waitForElement(() => getByText('๋ก๋ฉ์ค...')); // ๋ก๋ฉ์ค.. ๋ฌธ๊ตฌ ๋ณด์ฌ์ค์ผํจ
await waitForElement(() => getByText('Bret')); // Bret (username) ์ ๋ณด์ฌ์ค์ผํจ
});
});mock.onGet('/users').replyOnce(200, users);์ด๋ ๊ฒ ํ๋ฉด ์์ฒญ์ ๋ฑ ํ๋ฒ๋ง mocking ํ ์ ์์ต๋๋ค. ํ๋ฒ ์์ฒญ์ ํ๊ณ ๋๋ฉด ๊ทธ ๋ค์ ์์ฒญ์ ์ ์์ ์ผ๋ก ์์ฒญ์ด ๋ฉ๋๋ค.
mock
.onGet('/users')
.replyOnce(200, users) // ์ฒซ๋ฒ์งธ ์์ฒญ
.onGet('/users')
.replyOnce(500); // ๋๋ฒ์งธ ์์ฒญ์ด๋ ๊ฒ ํ๋ฉด ์ฒซ๋ฒ์งธ ์์ฒญ๊ณผ ๋๋ฒ์งธ ์์ฒญ์ ์ฐ๋ฌ์์ ์ค์ ํ ์ ์์ต๋๋ค. ์์ฒญ์ ์ฌ๋ฌ๋ฒ ํด์ผ ํ๋ ๊ฒฝ์ฐ ์ด๋ฐ ํํ๋ก ๊ตฌํํ์๋ฉด ๋ฉ๋๋ค.
๋ณดํต ๋ฉ์๋์ ๋ฐ๋ผ onGet(), onPost() ์ด๋ฐ์์ผ๋ก ์ฌ์ฉํ๋๋ฐ์, onAny() ๋ฅผ ์ฌ์ฉํ๋ฉด ์ด๋ค ๋ฉ์๋๋ mocking ์ ํ ์ ์์ต๋๋ค.
mock.onAny('/foo').reply(200);
// ์ฃผ์๋ฅผ ์๋ตํ๋ฉด ์ด๋ค ์ฃผ์๋ mocking ํฉ๋๋ค.
mock.onAny().reply(200);reset์ mock ์ธ์คํด์ค์ ๋ฑ๋ก๋ ๋ชจ๋ mock ํธ๋ค๋ฌ๋ฅผ ์ ๊ฑฐํฉ๋๋ค. ๋ง์ฝ์ ํ ์คํธ ์ผ์ด์ค๋ณ๋ก ๋ค๋ฅธ mock ์ค์ ์ ํ๊ณ ์ถ์ผ์๋ฉด ์ด ํจ์๋ฅผ ์ฌ์ฉํ์๋ฉด ๋ฉ๋๋ค.
mock.reset();restore์ axios ์์ mocking ๊ธฐ๋ฅ์ ์์ ํ ์ ๊ฑฐํฉ๋๋ค. ๋ง์ฝ์ ์ค์ ํ ์คํธ๋ฅผ ํ๋ค๊ฐ ์์ฒญ์ด ์ค์ ๋ก ๋ ๋ผ๊ฐ๊ฒ ํ๊ณ ์ถ์ผ๋ฉด ์ด ํจ์๋ฅผ ์ฌ์ฉํ๋ฉด ๋ฉ๋๋ค.
mock.restore();