[React.js] React-Testing-Library 사용하기
React-Testing-Library(RTL)
- CRA(create-react-app)에 기본적으로 내포되어 있다.
별도 설치 필요시 아래 명령어로 설치 가능하다.
yarn add --dev @testing-library/react
- Jest와 상호 보완적인 관계다.
Jest를 통해 전반적인 기능은 테스트가 가능하나,React 컴포넌트를 렌더링하고 테스트하기 위해서는 몇가지 기능이 더 필요하기 때문이다.
(React에서는 Enzyme 말고 RTL을 사용하면 되는 것 같다.)
Jest : 자체적인 Test Runner와 Test Util 제공
RTL : Jest + React 컴포넌트 Util 제공
엄밀히 말하자면 RTL이 Jest를 포함하고 있는 구조다.
Query
Rendering된 DOM Node에 접근하여 Element를 가져오는 메서드다.
RTL에 내포되어 있는 쿼리중 하나인 'getAllByRole' 쿼리를 해석해보면 아래와 같다.
get(Query-Type) / All(Target-Count) / ByRole(Target Type)
Query-Type
- get : 동기적 처리, 못찾으면 오류 발생
- find : 비동기적 처리, 못찾으면 오류 발생
- query : 동기적 처리, 못찾으면 NULL로 반환
Target-Count
- Query의 결과가 2개 이상일 경우 반드시 'All' 키워드를 붙여줘야 한다.
1개일 경우에는 안붙여도 상관 없으나, 만약 2개가 나올 경우 오류가 발생된다.
Target-Type(우선 순위 별 배치)
<span data-testid="id-label-1" role="id-label" aria-label="label">y0ngha</span>
- ByRole : Tag 내에 표기되어 있는 role에 맞게 갖고온다.
그렇다고 해서 억지로 role을 선언할 필요는 없다.
몇몇 시멘틱 태그는 implicit role이 있기 때문이다. - ByLabelText : aria-label, label Tag를 가져온다.
- ByPlaceHolderText : input Tag 내 placeHolder를 가져온다.
- ByText : text-content 를 가져온다.
- ByDisplayValue : 입력된 값을 가져온다.(radio-box : selected, check-box : checked, input, text-aria...)
- ByAltText : img Tag 내 Alt를 가져온다.
- ByTitle : svg Tag 내 Title Tag를 가져온다.
- ByTestId : data-testid 를 가져온다.
외에도 querySelector로 가져올 DOM Node에 Class Name, ID, Tag Name으로 가져올 수 있다.
예) querySelector(".id")
How to use?
// 1번 방법
describe("Test Component", () => {
it("render test", () => {
const wrap = render(<Test />);
const button = wrap.getByRole("button", { name : "+" }); // "+" 라는 이름을 가진 버튼 태그를 가져옴
})
})
// 2번 방법
describe("Test Component", () => {
it("render test", () => {
const wrap = render(<Test />);
const button = getByRole(wrap.container, "button", { name : "+" }); // "+" 라는 이름을 가진 버튼 태그를 가져옴
})
})
그 외에도 render 함수를 wrap이라는 변수에 담지 않고 'screen'으로 사용하는 방법이 있는데 권장하지 않는다.(가독성이 떨어짐)
Action
RTL은 Query로 얻어온 타겟으로 Event를 발생시킬 수 있다.
.
.
const wrap = render(<Test />);
const button = wrap.getByRole("button", { name : "+" });
userEvent.click(button); // 버튼 클릭 이벤트 발생
fireEvent 메서드도 존재하지만, userEvent 메서드를 사용할 것을 권장한다.
userEvent 메서드 내부에서 fireEvent를 사용중이기 때문이다.
Assertion
Jest 문법을 사용하면 된다.
→ expect();
Asynchronous Test
descrbie("Test Component", () => {
it("listItem Async Test", async () => {
expect(await wrap.findAllByRole("listitem")).toHaveLength(2);
})
})
다른 방법으로 await waitFor(() => ...) 방법이 있기는 하나, 가독성이 떨어져 위 방법으로 채택하여 사용되는 추세다.
Tip) Front-end(FE)에서 Back-end(BE)로 API 호출 후 테스트를 진행할 때에는 "성공 했는가" 에 초점을 두는 것이 아닌, "대기", "성공", "실패" 에 따른 컴포넌트 반응을 초점으로 작성해야한다.
Input Tag Change
.
.
const wrap = render(<Test />);
const input = wrap.getByRole("input")
userEvent.change(input, { target: { value: 'Hello' } });
expect(input).toHaveValue('Hello');
expect(input).toHaveAttribute('value', 'Hello');
Callback Mock Test
describe('Test Component', () => {
it('Callback Mock Test', () => {
const onClick = jest.fn();
const wrap = render(<Test onClick={onClick} />);
const button = wrap.getByRole("button", { name: "Click" });
userEvent.click(button);
expect(onClick).toBeCalledWith("Hello");
});
});
(실제 컴포넌트에는 <button onClick={onClick}>Click</button> Element가 있음.)
Axios Mock Test
yarn add --dev axios-mock-adapter
위 패키지 설치 후 아래와 같이 사용하면 된다.
.
.
const mock = new MockAdapter(axios, { delayResponse: 200 }); // 200ms 응답 딜레이
const url = "..."; // Api 주소
const mockData = {
id: 1,
name: "y0ngha"
};
mock.onGet(url).reply(200, mockData); // url에 대한 GET 응답값을 mockData로 설정
it("axios mock test", async() => {
.
.
async/await load 방법 사용(await find...)
.
.
})
위 코드는 테스트하는 컴포넌트에 아래와 같이 코드가 있을 때 사용되는 코드다.
function Test() {
useEffect(() => {
axios
.get(url)
.then(() => { // event });
}, []);
.
.
.
}
(즉시 실행된 axios 호출에 대해 테스트한 것)
Custom Hook
yarn add --dev @testing-library/react-hooks react-test-renderer
# typescript 사용시 react-test-renderer 대신 @types/react-test-renderer
위 패키지를 설치해야 한다.
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;
(테스트 Custom Hook)
.
.
.
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 Test', 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');
});
.
.
.
참고문서)
https://testing-library.com/docs/queries/byrole
https://tecoble.techcourse.co.kr/post/2021-10-22-react-testing-library/