React - 컴포넌트 테스팅 (React Testing Library)

goodjam92·2023년 4월 5일
0

React

목록 보기
7/7
post-thumbnail

서론

이전 글에서 테스트 코드가 무엇인지 왜 작성해야 하는지 알아보면서 Javascript에서 테스트 코드의 맛보기를 해보았다. 이번에는 React에서 컴포넌트를 테스팅하는 것을 알아보려고 한다.

React에서 많이 알려진 테스팅 프레임워크, 라이브러리가 있는데 바로 EnzymeReact Testing Library이다.

  • React Testing Library
    • React 를 만든 Facebook에 권장하는 테스팅 라이브러리
  • Enzyme
    • React v16 이하에서 가장 많이 사용했었던 테스팅 프레임워크

Enzyme은 공식적으로 React v16까지만 개발되었다. v17, 18에서 지원하지는 않는다. 하지만 비공식적으로 v17, 18에서 Enzyme을 사용할 수 있게 게시해둔 것이 있다. Enzyme이 궁금해서 한 번 React v18에서 써보다가 위와 같은 내용을 알게되었다는 것..ㅎㅎ 그럼 시작해보자!

React Testing Library

react-testing-library는 모든 테스트를 DOM 위주로 진행한다. 컴포넌트의 props나 state를 조회하는 일이 없다. 컴포넌트를 리팩토링 하게 될 때는 주로 내부 구조와 네이밍은 많이 바뀔 순 있어도 실제 작동 방식은 크게 바뀌지 않는데 이러한 특징을 따라 컴포넌트의 기능만 똑같이 작동하면 테스트는 실패하지 않는다. react-testing-library는 필요한 기능만 있어 매우 가벼운 테스팅 라이브러리이다.

1. 설치

설치는 간단한 방법으로는 CRA create react app으로 리액트 프로젝트를 시작하면 패키지 안에 포함되어 있기에 자동으로 설치가 된다.

수동으로 설치하려면 아래와 같다.

yarn add --dev @testing-library/react
yarn add --dev @testing-library/jest-dom
(or)
npm install --save-dev @testing-library/react ...

2. 설정

setupTests.js

import "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";

해당 파일에서 위와 같이 코드를 입력하여 테스트 라이브러리를 import 한다. 2번째 줄의 경우 jest에서 DOM 관련 matcher를 사용할 수 있게 해준다.

matcher

  • 여러 방법으로 값을 테스트 하도록하는 메서드
  • 기대한 값이 실제 반환된 값과 일치하는 지 확인하는 작업

3. 테스트 코드 작성하기

Info.js
props로 nickname과 name을 받아서 렌더링해주는 컴포넌트 작성

const Info = ({nickname, name}) => {
 return (
   <div>
     <b>{nickname}</b>
   	 <span>({name})</span>
   </div>
   )
}

Info.test.js
Info 컴포넌트의 테스트 코드를 작성

import { cleanup, render } from "@testing-library/react";
import Profile from "./Profile";


afterEach(() => {
  cleanup(); 		  
});	

/* cleanup
테스팅 라이브러리에서는 리액트에서 DOM 시뮬레이션을 위해 
JSDOM이라는 도구를 사용하여 `document.body`에 컴포넌트를 렌더링한다. 
이 때 각 테스트 케이스가 끝날때마다 기존 가상의 화면에 남아있는 
UI를 정리하기 위한 것이다.
*/

describe("<Profile />", () => {
  it("스냅샷 테스팅", () => {
    const utils = render(<Profile userName={"goodjam"} name={"재모"} />);
    expect(utils.container).toMatchSnapshot();
  }); // 스냅샷을 기록

  it("올바른 props 입력을 확인", () => {
    const { getByText } = render(
      <Profile userName={"goodjam"} name={"재모"} />
    );
    expect(getByText("goodjam")).toBeTruthy(); // goodjam 텍스트를 가진 엘리먼트가 있는지 확인
    expect(getByText("(재모)")).toBeTruthy(); // (JAEMO) 텍스트를 가진 엘리먼트가 있는지 확인
    expect(getByText(/모/)).toBeTruthy(); //정규식도 사용 가능
  });
});

위 코드에서 사용 된 matchers 참고

  • expect()
    • 테스트 시 특정 조건을 충족하는지 확인하는 matcher에 대한 액세스 제공
  • toMatchSnapshot()
    • 스냅샷 테스팅을 위해 스냅샷 파일 생성
  • toBeTruthy()
    • expect 내부의 조건이 true인지 확인

테스팅 라이브러리에서 컴포넌트를 렌더링 할 때 render() 함수를 사용한다. 이 함수가 호출되고 결과물에는 DOM을 선택할 수 있는 다양한 쿼리들과 container가 포함되어있는데 container는 해당 컴포넌트의 최상위 돔을 가리킨다. 이를 가지고 스냅샷 테스트를 할 수도 있다.

그리고 getByText()의 경우 쿼리 함수라고 부르는데 이 함수를 사용하면 텍스트를 이용해서 원하는 DOM을 선택 할 수 있다. 쿼리 함수는 종류가 다양하기에 따로 정리하려고 한다.

4. 스냅샷 테스팅

스냅샷 테스팅이란, 렌더링 된 결과가 이전에 렌더링한 결과와 일치하는지 확인하는 작업을 말한다. 위의 테스트 코드를 저장하고 yarn test를 실행하면 src/__snapshots__/Info.test.js.snap이라는 파일이 생성된다.

컴포넌트가 렌더링되고 이 스냅샷과 일치하지 않으면 테스트가 실패한다. 스냅샷을 업데이트 하고싶다면 테스트가 실행되고 있을 때 콘솔창에서 u키를 누른다.

5. Counter 컴포넌트 테스트 코드

이번에는 버튼을 누르면 값이 증가, 감소하는 기능을 가진 Counter컴포넌트의 테스트 코드를 작성해보자.

Counter.js

const Counter = () => {
  const [number, setNumber] = useState(0);
  
  const onIncrease = useCallback(()=> {
  	setNumber(number + 1);
  }, [number]);

  const onDecrease = useCallback(()=> {
   	setNumber(number - 1); 
  }, [number])
  
  return (
  	<div>
      <h2>{number}</h2>
      <button onClick={onIncrease}>+1</button>
	  <button onClick={onDecrease}>-1</button>
    </div>
  )
}

export default Counter;

Counter 컴포넌트를 작성하였으면 App에 컴포넌트를 추가하여 렌더링이 잘 되는지 확인한 뒤에 테스트 코드를 작성한다.

describe("<Counter />", () => {
  it("스냅샷 테스팅", () => {
    const utils = render(<Counter />);
    expect(utils.container).toMatchSnapshot();
  });

  it("버튼과 숫자가 있는지 확인하는 테스트", () => {
    const { getByText } = render(<Counter />);
    expect(getByText("0")).toBeTruthy();
    expect(getByText("+1")).toBeTruthy();
    expect(getByText("-1")).toBeTruthy();
  });

  it("plus 버튼 기능 테스트", () => {
    const utils = render(<Counter />);
    const number = utils.getByText("0");
    const plusButton = utils.getByText("+1");
    // 클릭 이벤트 두 번 발생시키기
    fireEvent.click(plusButton); 
    fireEvent.click(plusButton);

    expect(number).toHaveTextContent("2"); // jest-dom 확장 matcher 사용
    expect(number.textContent).toBe("2"); // textContent를 직접 비교
  });

  it("minus 버튼 기능 테스트", () => {
    const utils = render(<Counter />);
    const number = utils.getByText("0");
    const minusButton = utils.getByText("-1");
    // 클릭 이벤트 두 번 발생시키기
    fireEvent.click(minusButton);
    fireEvent.click(minusButton);
    expect(number).toHaveTextContent("-2");
  });
});

위 코드에서 사용 된 matchers 참고

  • fireEvent.이벤트이름(DOM, 이벤트객체);
    • 이벤트 발생시키는 함수
  • change 이벤트의 경우
    • fireEvent.change(input, { target: { value: "goodjam" } });
  • toHaveTextContent()
    • 엘리먼트 속 텍스트가 예상(expect)과 일치하는지 검증

테스트 코드까지 모두 작성하고 테스트가 통과하는지 확인해본다.

6. 정리

이렇게 React에서 권장하는 테스팅 라이브러리를 이용하여 테스트를 진행해보았다.
react-testing-library는 사용자가 애플리케이션을 이용하는 관점에서 사용자의 실제 경험 위주로 테스트를 작성한다. 따라서, 사용자에게 어떤 컨텐츠가 현재 보이고, 사용자가 어떤 이벤트를 발생시켰을 때, 그에 따라 화면에 변화가 일어나는지를 테스트 한다.
테스트 코드를 작성할 때 사용자가 겪는 실제 경험 순서대로 작성하는 것이 좋다.

이 글에선 컴포넌트 작성 -> 테스트 코드 작성으로 작업하였지만 TDD 방식대로 개발을 진행하게 되면 테스트 코드 -> 컴포넌트 작성으로 진행된다.

끝으로

React에서 권장하는 테스팅 라이브러리를 사용해서 React 컴포넌트를 테스트를 해보았다.
이전에 javascript에서 사용 한 jest와 굉장히 유사하다보니 코드가 친숙하게 보여 학습하는데 어려움은 덜하였다. 앞으로도 연습도 하고, 프로젝트를 진행할 때 테스트 코드를 작성해가면서 좀 더 테스트 코드를 친숙하게 다룰 수 있었으면 좋겠다.

.
.
.
.
.
.
.

참고 사이트
벨로퍼트와 함께하는 리액트 테스팅
LogRocket - Comparing React testing libraries
DaleSeo - React Testing Library 사용법

profile
습관을 들이도록 노력하자!

0개의 댓글