📌 Mock Fn(모의함수)란?

테스트를 위한 함수로, 원하는 함수의 로직을 테스트 하려면 작성해야할 코드가 많지만 테스트 함수를 사용하여 원하는 테스트를 시험할 코드만 간략하게 작성하여 테스트해볼 수 있어 편리하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
test("`onToggle` 속성(prop)에 연결된 함수가 정상적으로 실행됩니다.", () => {
let expected = "triggering toggle event";
let received = "";

render(
<ToggleButton
onToggle={() => {
received = expected;
}}
/>
);

expect(received).not.toBe(expected);

const element = screen.queryByRole("button");

fireEvent.click(element); // click button element

expect(received).toBe(expected);
});
  • 위 예제는 버튼이 클릭되었을 때, 해당 문구가 할당이 제대로 되는지 확인하는 테스트 코드이다.

테스트 목적은 버튼 클릭이 제대로 인식되는지, 선택된 요소가 예상된 값으로 할당이 되는지를 확인한다.

이렇게 테스트 코드를 작성하는 것은 번거로운 일이다.

그러므로 간략하게 테스트 함수를 생성하여 이벤트가 발생할 때, 모의 함수가 호출되면 앞서 말한 로직을 구현해줄 수 있으므로 버튼이 클릭되었을 때, 모의 함수 호출이 제대로 되는지만 확인하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
// mock Fn
test("`onToggle` 속성(prop)에 연결된 함수가 정상적으로 실행됩니다.", () => {
const mockFn = jest.fn();

render(<ToggleButton onToggle={mockFn} />);

const element = screen.getByRole("button");

fireEvent.click(element); // click button element

expect(mockFn).toHaveBeenCalled();
});

✏️ React VirtualDOM Test

리액트에서는 VitualDOM에 렌더링을 하여 RealDOM과 비교하기 때문에 Test시에도 RealDOM이 아닌 VirtualDOM에 렌더링된 것으로 테스트를 해줘야한다.

1
2
3
4
5
6
7
test("컴포넌트는 기본적으로 <span> 요소로 렌더링 됩니다.", () => {
render(<A11yHidden data-testid="tester" />);

const element = screen.getByTestId("tester");

expect(element.localName).toBe("span");
});
  • RealDOM에서 querySelector()로 요소를 가져오는 것처럼 VirtualDOM에서는 screen으로 요소를 가져올 수 있다.
  • 해당 요소를 가져오기 위해서 data-testId 값을 사용하였다.

❗️ localName vs nodeName 차이

  • localName : 소문자를 반환
  • nodeName : 대문자를 반환

defaultProps 사용하기

리액트 컴포넌트에서는 props에 default 값을 주기 위해서 defaultProps를 사용한다.

직접 파라미터에 App({ as: ComponentName = 'div' }) 이런식으로 주는 것을 안티패턴으로 본다.

1
2
3
4
5
6
7
8
9
10
11
import styles from "./A11yHidden.module.css";

export function A11yHidden({ as: ComponentName, ...restProps }) {
return (
<ComponentName className={styles.container} {...restProps}></ComponentName>
);
}

A11yHidden.defaultProps = {
as: "span",
};
  • as 라는 props를 받으면 해당하는 ComponentName으로 태그를 감싸서 컴포넌트를 생성한다.

component는 PascalCase 명명 규칙을 따르기 때문에 이를 따르지 않으면 에러를 발생시킨다.

getByRole vs getByTestId

기본적으로 div, span 같은 태그는 Role(역할)이 없다. 테스트 케이스에서 이러한 요소를 가져오기 위해서는 Role이 없으니 다른 방법을 찾아야한다.

그에 대한 대안으로 테스트용 data-testId="tester" 값을 주고 getByTestId()를 사용하여 요소를 가져올 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// div 요소 가져올 경우
test('컴포넌트의 `as` 속성(prop) 값이 "div"인 경우, <div> 요소로 렌더링 됩니다.', () => {
render(<Banner as="div" data-testid="tester" />);

const element = screen.getByTestId("tester");
expect(element.localName).toBe("div");
});

// figure 요소 가져올 경우
test("컴포넌트는 기본적으로 <figure> 요소로 렌더링 됩니다.", () => {
render(<Banner />);

const element = screen.getByRole("figure");
expect(element.localName).toBe("figure");
});

figure, img 등 우리가 직접 Role을 선정해주지 않아도 브라우저에서 해당 태그들을 해석할 때 Role을 할당해주기 때문에 이러한 요소들은 getByRole()를 이용하여 가져올 수 있다.

🏓 소감

마침 우테코에서도 Jest를 사용하고 있었는데, 수업시간 때 리액트 DOM 테스트를 해볼 수 있는 기회가 있어서 유익했다. 프론트 엔드에서는 사용자의 행동에 의해 UI가 변경되는 요소들이 많기 때문에 이러한 사용자 액션에 의한 test를 하기 어렵다.

이럴 경우 사용자가 행동하는 것처럼 테스트를 진행하는 storyBook이라는 testing Tool도 있으니 Jest가 익숙해지면 storyBook도 사용해봐야겠다.

댓글 공유

React Testing Library

카테고리 React

📌 React Testing Library

리액트에서 TDD 방식의 개발을 하기 위해서 테스팅 라이브러리를 사용해보자.

🎯 목적

1. 버그 캐치

테스트를 통해 예상치 못한 여러가지 버그들을 사전에 확인하기 용이하다.

2. 애플리케이션 신뢰도 향상

어떠한 근거로 이 애플리케이션이 제대로 동작하는지 근거를 뒷받침하고 이 근거에 대한 신뢰도를 높일 수 있다.

3. 질문 및 답변 시간 축소

누군가 어떤 컴포넌트가 어떻게 동작하는지 질문했을 때 그에대한 답변으로 이 테스트를 보여주기만 하면된다. 그럼으로 시간을 절약하고 유지보수성을 높일 수 있다.

4. 문서 역할

테스트라는 문서를 제공함으로서 2,3번의 장점을 가능하도록 한다.

💼 사용방법

우선 해당 라이브러리를 설치해준다.

1
2
3
4
5
npm install --save-dev @testing-library/react

npm install --save-dev @testing-library/dom

npm install --save-dev @testing-library/user-event

리액트 테스팅 라이브리러DOM 테스트 라이브러리, 사용자 행동 테스트 라이브러리를 설치하여 테스트 개발을 해보자.

🦖 Component 테스트

✏️ 테스트 코드 작성하는 방법

  1. 테스트가 필요한 컴포넌트 렌더링

  2. 컴포넌트의 요소 탐색

  3. 요소와의 상호작용

  4. 어설션 테스트 결과와 기대 값이 일치하는 지 확인

우선 컴포넌트를 생성해주자.

📌 Tip 컴포넌트 생성

컴포넌트를 쉽게 생성하기 위해 yamoo9님이 제공해준 Tool을 사용해보도록하자.

1
npx degit yamoo9/create-react-component create-react-component
1
2
3
4
5
6
7
8
// package.json
{
...
"scripts" :
...,
"rc": "node create-react-component create",
"rd": "node create-react-component delete"
}
1
2
npm run rc -- 컴포넌트_이름 // 컴포넌트 생성
npm run rd -- 컴포넌트_이름 // 컴포넌트 제거

ESLint 에서 테스팅 라이브러리를 사용하게되면 오류를 띄워주는데 이에 대한 Lint 경고를 꺼두자.

1
2
3
4
5
// package.json
{
"eslintConfig":{...},
"testing-library/no-debugging-utils": "off"
}

예제

1. 컴포넌트가 렌더링 확인

1
2
3
4
// ToggleButton.jsx
export function ToggleButton({ onText, offText, on }) {
return <div>{on ? onText : offText}</div>;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ToggleButton.test.jsx
import { render, screen } from "@testing-library/react";
import { ToggleButton } from "./ToggleButton";

describe.only("ToggleButton Test Start!", () => {
test("컴포넌트가 정상적으로 렌더링 되었습니다.", () => {
render(<ToggleButton onText="1" offText="0" />);

const offTextElement = screen.getByText("0");
const onTextElement = screen.queryByText("1");

expect(offTextElement).toBeInTheDocument();
expect(onTextElement).not.toBeInTheDocument();
});
});
  • getByText() : 가상으로 그려진 문서에 존재하는 것만 가져올 수 있다. 만약 존재 하지 않는 다면 오류를 발생시킨다.
  • queryByText() : 존재하지 않으면 오류를 발생시키지 않고 null 값으로 가져온다.

2. 활성화 상태 여부에 따라 텍스트 표시

1
2
3
4
// ToggleButton.jsx
export function ToggleButton({ onText, offText, on }) {
return <button type="button">{on ? onText : offText}</button>;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ToggleButton.test.jsx
describe("ToggleButton 컴포넌트", () => {
test("활성 상태 여부에 따라 활성(ON)/비활성(OFF) 텍스트가 표시됩니다.", () => {
let onText = "ON";
let offText = "OFF";

render(<ToggleButton onText={onText} offText={offText} />);

let elements = screen.queryAllByRole("button");
let firstElement = elements[0];
expect(firstElement).toHaveTextContent(offText);

screen.debug();

cleanup();

render(<ToggleButton onText={onText} offText={offText} on />);

elements = screen.queryAllByRole("button");
firstElement = elements[0];

screen.debug();

expect(firstElement).toHaveTextContent(onText);
});
});
  • 만약 on 일때의 text와 off일 때의 text를 찾고 싶다면 queryAllText() 를 사용해준다,
  • cleanup()을 해줘야지만 firstElement를 확인할 때, 앞에 그려진 것을 지우고 새로 그려진 것을 비교해줄 수 있다.

3. onToggle 속성(prop)에 연결된 함수 실행 확인 & 활성 상태 컴포넌트는 ToggleButton--on 클래스 이름 포함 확인

1
2
3
4
5
6
7
8
9
10
11
12
// ToggleButton.js
export function ToggleButton({ onText, offText, on, onToggle }) {
return (
<button
type="button"
className={`ToggleButton ${on ? "ToggleButton--on" : ""}`.trim()}
onClick={onToggle}
>
{on ? onText : offText}
</button>
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// ToggleButton.test.js
describe("ToggleButton 컴포넌트", () => {
test("`onToggle` 속성(prop)에 연결된 함수가 정상적으로 실행됩니다.", () => {
let expected = "triggering toggle event";
let received = "";

render(
<ToggleButton
onToggle={() => {
received = expected;
}}
/>
);

expect(received).not.toBe(expected);

const element = screen.queryByRole("button");

fireEvent.click(element); // click button element

expect(received).toBe(expected);
});

test(`활성 상태의 컴포넌트는 'ToggleButton--on' 클래스 이름을 포함한다.`, () => {
let expected = "ToggleButton--on";
render(<ToggleButton on />);

const element = screen.getByRole("button");
expect(element).toHaveClass(expected);
});
});
  • fireEvent() : 이벤트를 발생시켜주는 메서드이다.

  • queryByRole() : 해당 요소의 역할을 확인하는데 사용된다.

    ex) button 태그에 type을 “button”으로 명시적으로 작성하였는지…

🏓 소감

오늘 수업에서 상태를 가지지 않는 컴포넌트의 다양한 테스트 방법에 대해 실습을 진행하였다. 리액트를 TDD 방식으로 개발을 진행하게 된다면 앞서 말한 애플리케이션의 신뢰도 향상할 수 있고 테스트 문서를 생성하여 유지보수를 용이하게 할 수 있다는 생각이 들었다.

아직 jest에 익숙하지 않아 낯설고 어렵지만, 우테코에서도 jest를 사용하고 있고 앞으로 자주 사용해보면서 테스트 주도 개발에 대해 몸을 익히도록 해야겠다.

댓글 공유

  • page 1 of 1

loco9939

author.bio


author.job