ReactJS/Information

[React.js] React-Testing-Library 사용하기

y0ngha 2022. 1. 3. 21:46

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

RTLQuery로 얻어온 타겟으로 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/

https://sg-choi.tistory.com/276