반응형
react-testing-library 테스트 환경 설정
library 설치
yarn add --dev @testing-library/react @testing-library/jest-dom @testing-library/dom
setupTests.js 파일 생성
- 경로 : <프로젝트>/src/setupTests.js
import "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
package.json 설정 수정
{
...
"jest": {
...
"setupFilesAfterEnv": [
"<rootDir>/src/setupTests.js"
],
...
// alias로 경로를 지정해 사용할 경우 아래같은 설정이 없으면 오류 발생
"moduleNameMapper": {
"@components/(.*)": "<rootDir>/src/components/$1"
},
...
}
...
}
테스트 코드 작성
// index.js
import React from 'react';
const Test = () => {
return <div>Hi</div>;
};
export default Test;
// index.test.js
import React from 'react';
import { render } from '@testing-library/react';
import Test from '@components/Test';
describe('<Test />', () => {
it('matches snapshot', () => {
const utils = render(<Test />);
expect(utils.container).toMatchSnapshot();
});
});
테스트 실행
yarn test
테스트 코드 예제
Snapshot 테스트
// index.js
import React from 'react';
const Test = () => {
return <div>Hello World</div>;
};
export default Test;
// index.test.js
import React from 'react';
import { render } from '@testing-library/react';
import Test from '@components/Test';
describe('<Test />', () => {
// 최초 테스트 실행시 스냅샷 파일을 만들고
// 두 번째 테스트 실행시부터 생성된 스냅샷과 비교하여 일치하는지 테스트
// 스냅샷은 상대경로에 index.test.js.snap 파일로 저장됨
it('matches snapshot', () => {
const utils = render(<Test />);
expect(utils.container).toMatchSnapshot();
});
});
Element 존재 확인 테스트
// index.js
import React from 'react';
const Test = () => {
return <div>Hello World</div>;
};
export default Test;
// index.test.js
import React from 'react';
import { queryByText } from '@testing-library/dom';
import { render } from '@testing-library/react';
import Test from '@components/Test';
describe('<Test />', () => {
it('Element 존재 확인 테스트', () => {
const utils = render(<Test />);
const element = queryByText(utils.container, 'Hello World');
// const element = queryByText(
// utils.container,
// (content, element) => element.type === 'div' && content === 'Hello World'
// );
expect(element).toBeTruthy();
});
});
Style 존재 확인 테스트
// index.js
import React from 'react';
const Test = () => {
return <div style={{ textAlign: 'center' }}>Hello World</div>;
};
export default Test;
// index.test.js
import React from 'react';
import { queryByText } from '@testing-library/dom';
import { render } from '@testing-library/react';
import Test from '@components/Test';
describe('<Test />', () => {
it('Style 존재 확인 테스트', () => {
const utils = render(<Test />);
const element = queryByText(utils.container, 'Hello World');
expect(element).toHaveStyle('text-align: center;');
expect(element).not.toHaveStyle('display: none;');
});
});
Event trigger 테스트
// index.js
import React, { useState } from 'react';
const Test = () => {
const [message, setMessage] = useState('Hello World');
const [count, setCount] = useState(0);
return (
<div>
<input
type={'text'}
value={message}
onChange={e => setMessage(e.target.value)}
/>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
};
export default Test;
// index.test.js
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import Test from '@components/Test';
describe('<Test />', () => {
it('Event trigger 테스트', () => {
const utils = render(<Test />);
const input = utils.getByDisplayValue('Hello World');
const count = utils.getByText('0');
const addButton = utils.getByText('+1');
fireEvent.click(addButton);
fireEvent.click(addButton);
fireEvent.change(input, { target: { value: 'Bye' } });
expect(count).toHaveTextContent('2');
expect(input).toHaveValue('Bye');
expect(input).toHaveAttribute('value', 'Bye');
});
});
Callback Mock 테스트
// index.js
import React from 'react';
const Test = ({ onClick }) => {
return (
<div>
<button onClick={() => onClick('Hello')}>Click</button>
</div>
);
};
export default Test;
// index.test.js
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import Test from '@components/Test';
describe('<Test />', () => {
it('Callback Mocking 테스트', () => {
const onClick = jest.fn();
const utils = render(<Test onClick={onClick} />);
const button = utils.getByText('Click');
fireEvent.click(button);
expect(onClick).toBeCalledWith('Hello');
});
});
Module Mock 테스트
- student.js
const student = {
name: 'John',
greet: () => 'Hello'
};
export default student;
- index.js
import React from 'react';
import student from './student';
const Test = () => {
return (
<div>
<div>{student.name}</div>
<div>{student.greet()}</div>
</div>
);
};
export default Test;
- index.test.js
import React from 'react';
import { queryByText, render } from '@testing-library/react';
import Test from '@components/Test';
jest.mock('./student', () => {
return {
name: 'Tom',
greet: () => 'Hi'
};
});
describe('<Test />', () => {
it('module mock', () => {
const { container } = render(<Test />);
const name = queryByText(container, 'Tom');
const greet = queryByText(container, 'Hi');
expect(name).toBeInTheDocument();
expect(greet).toBeInTheDocument();
});
});
Async 테스트
// index.js
import React, { useState } from 'react';
const Test = () => {
const [isOn, setIsOn] = useState(false);
const handleClick = () => {
setTimeout(() => {
setIsOn(!isOn);
}, 2000);
};
return (
<div>
<span>{isOn ? 'ON' : 'OFF'}</span>
<button onClick={handleClick}>Switch</button>
</div>
);
};
export default Test;
// index.test.js
import React from 'react';
import { render, fireEvent, waitForElement } from '@testing-library/react';
import Test from '@components/Test';
describe('<Test />', () => {
it('Async 테스트', async () => {
const utils = render(<Test />);
const switchButton = utils.getByText('Switch');
fireEvent.click(switchButton);
const statusText = await waitForElement(() => utils.getByText('ON'));
expect(statusText).toHaveTextContent('ON');
});
});
Axios Mock 테스트
# library 설치
yarn add --dev axios-mock-adapter
// index.js
import React, { useEffect, useState } from 'react';
import axios from 'axios';
const Test = () => {
const [user, setUser] = useState(undefined);
useEffect(() => {
axios
.get('https://jsonplaceholder.typicode.com/users/1')
.then(({ data }) => setUser(data));
}, []);
return (
<div>
{user ? (
<div>
<div className={'name'}>name : {user.name}</div>
<div className={'email'}>email : {user.email}</div>
</div>
) : (
<div>Loading</div>
)}
</div>
);
};
export default Test;
// index.test.js
import React from 'react';
import { render, waitForElement } from '@testing-library/react';
import Test from '@components/Test';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
describe('<Test />', () => {
const mock = new MockAdapter(axios, { delayResponse: 200 }); // 200ms 가짜 딜레이 설정
// API 요청에 대하여 응답 미리 정하기
mock.onGet('https://jsonplaceholder.typicode.com/users/1').reply(200, {
id: 1,
name: 'Leanne Graham',
email: 'Sincere@april.biz'
});
it('Axios Mock 테스트', async () => {
const utils = render(<Test />);
const name = await waitForElement(() =>
utils.container.querySelector('.name')
);
const email = await waitForElement(() =>
utils.container.querySelector('.email')
);
expect(name).toHaveTextContent('name : Leanne Graham');
expect(email).toHaveTextContent('email : Sincere@april.biz');
});
});
Custom Hooks 테스트
# library 설치
yarn add --dev @testing-library/react-hooks react-test-renderer@^16.9.0
// useUserApi.js
import { useEffect, useState } from 'react';
import axios from 'axios';
const useUserApi = ({ id }) => {
const [user, setUser] = useState(undefined);
useEffect(() => {
if (id) {
axios
.get(`https://jsonplaceholder.typicode.com/users/${id}`)
.then(({ data }) => setUser(data));
}
}, [id]);
return {
user
};
};
export default useUserApi;
// index.test.js
import React from 'react';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
import { renderHook } from '@testing-library/react-hooks';
import useUserApi from '@components/Test/hooks/useUserApi';
describe('<Test />', () => {
const mock = new MockAdapter(axios, { delayResponse: 100 });
mock.onGet('https://jsonplaceholder.typicode.com/users/1')
.reply(200, {
id: 1,
name: 'Leanne Graham',
email: 'Sincere@april.biz'
})
.onGet('https://jsonplaceholder.typicode.com/users/2')
.reply(200, {
id: 2,
name: 'Ervin Howell',
email: 'Shanna@melissa.tv'
});
const setup = (defaultProps) => {
return renderHook((props) => useUserApi(props), {
initialProps: defaultProps
});
};
it('Custom Hooks 테스트', async () => {
const { result, rerender, waitForNextUpdate } = setup({ id: 1 });
expect(result.current.user).toBeUndefined();
await waitForNextUpdate();
expect(result.current.user.name).toEqual('Leanne Graham');
rerender({ id: 2 });
await waitForNextUpdate();
expect(result.current.user.name).toEqual('Ervin Howell');
});
});
Custom Hooks 테스트 - with context
// index.js
import React, { createContext, useState } from 'react';
import useUserApi from '@components/Test/hooks/useUserApi';
export const TestContext = createContext();
const Test = () => {
const [user, setUser] = useState(undefined);
const context = {
user,
setUser
};
return (
<TestContext.Provider value={context}>
<Child />
</TestContext.Provider>
);
};
const Child = () => {
const { user } = useUserApi({ id: 1 });
return (
<div>
{user ? (
<div>
<div className={'name'}>name : {user.name}</div>
<div className={'email'}>email : {user.email}</div>
</div>
) : (
<div>Loading</div>
)}
</div>
);
};
export default Test;
// useUserApi.js
import { useContext, useEffect } from 'react';
import axios from 'axios';
import { TestContext } from '@components/Test';
const useUserApi = ({ id }) => {
const { user, setUser } = useContext(TestContext);
useEffect(() => {
if (id) {
axios
.get(`https://jsonplaceholder.typicode.com/users/${id}`)
.then(({ data }) => setUser(data));
}
}, [id]);
return {
user
};
};
export default useUserApi;
// index.test.js
import React, { useState } from 'react';
import { renderHook } from '@testing-library/react-hooks';
import useUserApi from '@components/Test/hooks/useUserApi';
import { TestContext } from '@components/Test/index';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
describe('<Test />', () => {
const mock = new MockAdapter(axios);
mock.onGet('https://jsonplaceholder.typicode.com/users/1')
.reply(200, {
id: 1,
name: 'Leanne Graham',
email: 'Sincere@april.biz'
})
.onGet('https://jsonplaceholder.typicode.com/users/2')
.reply(200, {
id: 2,
name: 'Ervin Howell',
email: 'Shanna@melissa.tv'
});
const setup = (defaultProps) => {
return renderHook((props) => useUserApi(props), {
initialProps: defaultProps,
wrapper: ({ children }) => {
const [user, setUser] = useState(undefined);
const context = {
user,
setUser
};
return (
<TestContext.Provider value={context}>
{children}
</TestContext.Provider>
);
}
});
};
it('Custom Hooks 테스트 - with context', async () => {
const { result, rerender, waitForNextUpdate } = setup({ id: 1 });
expect(result.current.user).toBeUndefined();
await waitForNextUpdate();
expect(result.current.user.name).toEqual('Leanne Graham');
rerender({ id: 2 });
await waitForNextUpdate();
expect(result.current.user.name).toEqual('Ervin Howell');
});
});
이슈
“require.context is not a function” 오류 발생
- 내부적으로 require.context()를 사용하는 경우 오류 발생
- require.context()는 webpack 함수이므로 jest 실행시 올바르게 동작하지 않음
- 해결 방법
참고
- https://velog.io/@velopert/tdd-with-react-testing-library
- https://velog.io/@velopert/react-testing-library
- https://velog.io/@velopert/react-testing-library-%EC%9D%98-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%9E%91%EC%97%85%EC%9D%84-%EC%9C%84%ED%95%9C-%ED%85%8C%EC%8A%A4%ED%8A%B8
- https://github.com/testing-library/react-hooks-testing-library
- https://doppelmutzi.github.io/testing-custom-react-hooks/
- https://github.com/testing-library/react-hooks-testing-library/blob/master/docs/usage/basic-hooks.md
- https://www.newline.co/@jamesfulford/testing-custom-react-hooks-with-jest–8372a502
- https://testing-library.com/docs/dom-testing-library/api-queries
반응형
'Development > React' 카테고리의 다른 글
[React] react-redux (0) | 2020.12.30 |
---|---|
[React] Library (0) | 2020.12.30 |
[React] Enzyme (0) | 2019.12.28 |
[React] Setting (0) | 2019.09.08 |
[React] ETC (0) | 2019.08.29 |