[Jest] React Testing Library를 사용하여 DOM 테스팅하기

Quartz 쿼츠·2022년 12월 28일
1
post-thumbnail

0. Introduction

Jest를 사용하면서 대부분 함수 단위의 유닛 테스트나 통합 테스트만 사용했었다. 팀 단위 프로젝트의 CI/CD를 구상하면서 리액트 컴포넌트가 잘 렌더링되고 있는지 확인이 필요했고, CRA에서 기본적으로 지원하는 React Testing Library를 사용해보기로 하였다.

1. 기본 개념

본 라이브러리를 학습하기 위해 코딩 앙마님의 React Testing Library 강의 영상을 수강하였고, 기본 개념과 코드들은 해당 영상을 참고하여 작성하였다.

1.1 컴포넌트 렌더링 테스트

가장 기본적인 App 컴포넌트가 렌더링되고 있는지에 대한 테스트를 작성해보자. 아래의 테스트는 App 컴포넌트를 렌더링하여 heading역할을 하는 요소를 받아온다. 그리고 해당 엘리먼트가 화면에 존재하는지 검증하면 테스트가 종료된다.

  • render() 특정 컴포넌트를 렌더링
  • screen 객체의 쿼리 메소드로 HTML element에 접근
// App.test.js
import { render, screen } from '@testing-library/react';

test('<App /> 렌더링시 / 경로로 렌더링 되나요?', async () => {
    render(<App />);

    const headingEl = screen.getByRole('heading');
    expect(headingEl).toBeInTheDocument();
  });

1.2 jest-dom의 custom matchers

toBeInTheDocument와 같이 matcher를 통해 특정 DOM 요소의 상태를 테스트할 수 있으며, 공식 문서에서는 다양한 matcher를 제공하고 있다. 회원이 아닌 경우 버튼을 비활성화하고 안내 문구를 빨간색으로 나타내는 JoinBtn 컴포넌트를 테스트해보자.

test("회원이 아닌 경우 버튼을 비활성화합니다. 안내 문구는 빨간색입니다.", () =>{
	render(<JoinBtn isMember={false}/>);

	const btnEl = screen.getByRole("button");
	const textEl = screen.getByRole("heading");
	
	expect(btnEl).toBeInTheDocument(); // 해당 버튼이 화면에 존재하나요?
	expect(textEl).toBeInTheDocument(); // 해당 문구가 화면에 존재하나요?
	expect(btnEl).toBeDisbaled(); // 버튼 비활성화 되어있나요?
	expect(btnEl).toHaveStyle({ // 문구가 빨간색인가요?
		color: "red",
	});
})

테스트에 용이한 컴포넌트를 위해서 가변적인 데이터를 props로 외부에서 주입하자!

1.3 HTML element를 찾는 쿼리

렌더링된 컴포넌트에서 테스트를 위한 요소를 찾기 위해 공식 문서에서는 다양한 쿼리들을 제공한다. 이 쿼리들을 단일/여러개의 요소를 찾을 것인지, 요소를 어떤 방식으로 찾을 것인지에 따라 분류할 수 있다.


[표 1. Summary Table: Type of Query(출처: 공식 문서)]

1.3.1 단일 / 여러 개의 요소 찾기

하나의 요소를 찾는 쿼리들은 1 개의 요소가 매칭되었을 때 해당 요소를 반환하고, 여러 개가 매칭되면 [표 1]과 같이 에러를 발생시킨다. 요소를 찾는 방법은 역할, 텍스트, 지정한 테스트 id 등으로 다양하며 요소를 특정하기 위해 옵션을 추가하기도 한다. Wrapper로만 사용하는 div는 테스트 id를 붙여 찾을 수 있으나 테스트만을 위한 코드가 프로젝트에 추가되므로 최후의 방법으로 사용하는 것이 좋다.

<div data-testid="my-div">
  <h1>마이페이지</h1>
  <label htmlFor="username">이름</label>
  <input type="text" id="username" />
</div>
const textEl = screen.getByRole("heading",{
	level: 1, // h1 tag 찾기
});

const inputEl = screen.getByRole("textbox", {
	name: "이름", //label's children text 이름으로 찾기 - htmlFor id가 연결되어 있어야 함
});

const inputEl = screen.getByLabelText("이름");
// label이 아닌 연결된 textbox를 찾음

const inputEl = screen.getByLabelText("이름", {
	selector: input,
});

const inputElements = screen.getAllByRole("textbox"); // 배열을 요소로 반환

const divEl = screen.getByTextId("my-div");

1.3.2 여러 방식으로 요소 찾기

먼저 요소를 찾는 방식에 따라 아래와 같이 3 가지 타입으로 먼저 분류해보자.

  • get... 일치하는 요소가 없으면 에러 발생
  • query... 일치하는 요소가 없으면 null, 빈 배열 반환 ➡️ 없는 요소 테스트에 활용!
  • find... 프로미스를 반환(default = 1 초)
const liElements = screen.getAllByRole("listitem"); // 요소 없으면 아예 에러뜸
const liElements = screen.queryByRole("listitem"); // null 반환
const liElements = screen.queryAllByRole("listitem"); // 빈 배열 반환

test("잠시 후 제목이 나타납니다." async() =>{
	render(<UserList users={users}/>);
	screen.debug(); // 렌더링된 DOM 트리 확인할 수 있는 디버깅 모드
	const titleEl = await screen.findByRole("heading", {
		name: "사용자 목록"	
	},
	{
		timeout: 2000, // default = 1 초
	});
	screen.debug();
});

2. 팀 프로젝트에 적용해보기

2.1 테스트하기 좋은 코드로 리팩토링하기

해당 내용을 바탕으로 원티드 프리 온보딩 1 주차 과제Route 컴포넌트에 테스트를 간단하게 추가해보았다. Assignment3의 요구 조건에서는 로그인 여부(토큰 유무)에 따라 리다이렉트 처리를 한다. 우리 팀은 Router 컴포넌트 내에서 PublicRoutePrivateRoute 컴포넌트를 두어 토큰을 확인하고 리다이렉트 처리를 위임하였다.

PublicRoute의 좌측 코드도 잘 작동하지만 테스트를 적용하려면 #1.2와 같이 isLogin 값을 외부로부터 주입하는 것이 좋다.


따라서 Router 컴포넌트에서 토큰을 확인하는 역할을 갖고, PublicRoutePrivateRoute 컴포넌트는 리다이렉트 처리만 위임받게 리팩토링하였다.

2.2 테스트 코드를 잘 작성하기

라우팅에 대한 테스트 코드는 아래와 같이 작성하였다. Router 컴포넌트 내에 서 렌더링 되는 Route의 element를 테스트하였으며, 주의할 점은 이 컴포넌트들은 Navigate라는 react-router-dom 컴포넌트를 사용하므로BrowserRouter로 감싸주어야 한다.

테스트 코드도 코드이므로 재사용성이 좋게 잘 작성해야 한다는 말이 있다. getByRole 쿼리를 사용해 타이틀 요소를 가져올 때 option을 만드는 로직이 반복되므로 makeOptions 함수를 추출하여 상단에 작성하였다.

import { BrowserRouter } from 'react-router-dom';

const makeOptions = (level, name) => {
  return { level, name };
};

describe('Routing 테스트', () => {
  test('토큰이 없는 상태로 / 경로로 접근하면 <Home />이 렌더링 되나요?', () => {
    render(
      <PublicRoute isLogin={null}>
        <Home />
      </PublicRoute>,
      { wrapper: BrowserRouter },
    );
    const headingEl = screen.getByRole('heading', makeOptions(1, '회원 가입'));
    expect(headingEl).toBeInTheDocument();
  });

  test('토큰이 있는 상태로 /todo 경로로 접근하면 <Todos />가 렌더링 되나요?', () => {
    render(
      <PrivateRoute isLogin="someToken">
        <Todos />
      </PrivateRoute>,
      { wrapper: BrowserRouter },
    );

    const headingEl = screen.getByRole('heading', makeOptions(1, 'Todo list'));
    expect(headingEl).toBeInTheDocument();
  });
});

아주 기초적인 수준이지만 본 테스트로 브라우저 상에서 로그인하여 토큰을 만들고 다시 지워서 리다이렉션을 테스트하는 번거로움을 줄일 수 있다고 생각한다. 모든 코드를 테스트하면 좋겠지만 확인하기 번거로운 로직들을 우선적으로 테스트 코드를 작성하는 이런 방식의 차선책도 존재한다.

참고자료

profile
Code what we love. 좋아하는 것들을 구현하고 있는 프론트엔드 개발자입니다. 사용자도 함께 만족하는 서비스를 만들고 싶습니다.

0개의 댓글