스무디 한 잔 마시며 끝내는 리액트 + TDD - 4장 리액트 테스트 - react-testing-library

728x90

react-testing-library

Jest는 자바스크립트 테스트 프레임워크로 js를 전반적으로 테스트하기 위한 프레임워크이다. 리액트토 js이기는 하지만, JSX를 사용하고 있으므로 일반적인 js는 아니다. 또한, 리액트의 JSX는 HTML의 DOM을 다루기 때문에 단순한 js의 테스트로 정확한 오류를 잡아내기는 어렵다.

리액트뿐만 아니라 최근 프론트엔드(Frontend) 프레임워크, 라이브러리인 앵귤러와 Vue도 js에서 DOM을 직접 다루기 때문에 일반적인 js 테스트 프레임워크로는 모든 테스트를 수행하기 어렵다.

@testing-library는 이런 문제를 해결하고자 만들어진 DOM 테스팅 라이브러리(DOM Testing Library)이다. @tesing-library는 사용자 중심 방식으로 UI 컴포넌트를 테스트하는 데 도움을 주는 라이브러리이다.

@testing-library는 리액트 이외에도 앵귤러, Vue, Sevele 등 UI 컴포넌트를 테스트하기 위한 매우 가벼운 솔루션으로, 유지보수가 가능한 리액트 컴포넌트용 테스트 코드를 확인할 수 있다

react-testing-library의 장점

react-testing-library는 리액트 컴포넌트를 테스트하기 위한 매우 가벼운 솔루션으로, 유지보수가 가능한 리액트 컴포넌트용 테스트 코드를 작성할 수 있다.

react-testing-library도 같은 원리로 리액트 컴포넌트용 테스트 코드 작성을 도와준다. 이 말은 즉, 테스트 코드를 작성할 때 컴포넌트 세부 구현 사항을 포함하지 않으면서도 신뢰할 수 있는 테스트 코드 작성에 도움을 준다. 이렇게 컴포넌트의 세부 구현 사하을 포함하지 않은 테스트 코드를 작성하면 컴포넌트의 세부 구현 부분을 리팩토링하여도 테스트 코드를 수정할 필요가 없다. 이로 인해 한번 작성한 테스트 코드는 긴 시간 유지할 수 있으며 오랜 기간 유지 가능한 테스트 코드는 테스트 코드를 자주 수정하지 않아도 되므로 개발 생산성을 향상 시켜준다.

react-testing-library는 react-dom위에서 동작한다. 다른 테스트 프레임워크는 리액트 컴포넌트를 단순히 인스턴스로 처리하지만, react-testing-library는 실제 DOM 노드에서 작동하므로 더 신뢰할 수 있는 테스트를 할 수 있다.

react-testing-library는 사용자 중심의 테스트 유틸리티를 제공한다. 사용자 중심의 테스트 유틸리티란 react-testing-library를 사용하여 DOM을 찾는 기능들이 실제 사용자 DOM을 사용하는 방식과 유사한 형태로 제공되고 있음을 의미한다. 예를 들어, 텍스트로 폼(Form)의 요소를 찾거나 텍스트에서 링크 및 버튼 등을 찾는 테스트 코드는 마치 사용자가 화면을 보면서 찾는 것과 같은 형태로 테스트 코드를 작성할 수 있도록 돕는다.

react-testing-library는 테스트 실행기 또는 프레임워크가 아니다. 따라서 react-testing-library를 사용하기 위해서는 다른 테스트 프레임워크와 함께 사용해야 한다. react-testing-library의 공식 사이트에서는 함께 사용할 테스트 프레임워크로 Jest를 추천하고 있지만, react-testing-library는 특정 테스트 프레임워크에 종속되어 있지 않았으므로 Jest이 외에 다른 테스트 프레임워크에서도 동작한다.

프로젝트 준비

react-testing-library는 리액트의 컴포넌트를 테스트하기 위한 라이브러리이므로 react-testing-library를 사용하기 위해서는 리액트 프로젝트를 준비할 필요가 있다.

다음 명령어를 실행하여 react-testing-library를 사용할 리액트 리액트 프로젝트를 생성하자.

npx create-react-app react-testing-library-test

react-testing-library 설치

react-testing-library는 Jest와 마찬가지로 create-react-app으로 생성한 리액트 프로젝트에 기본적으로 같이 설치된다. 우리는 create-react-app을 사용하여 리액트 프로젝트를 생성하였으므로 추가적인 설치 과정을 진행하지 않아도 된다.

만약 create-react-app으로 프로젝트를 생성하지 않는 경우에는 다음의 명령어를 실행하여 react-testing-library를 설치해야 한다.

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

사용 방법

create-react-app으로 생성한 리액트 프로젝트의 src 폴더를 열어보면 리액트 컴포넌트가 작성된 App.js 파일과 그에 관한 테스트 코드가 작성된 App.test.js 파일을 확인할 수 있다.

우선, 리액트 컴포넌트가 작성된 App.js 파일을 열어 컴포넌트의 내용을 확인해 보자.

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and saved to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
            Learn React
        </a>
      </header>
    </div>
    );
}

이 App 컴포넌트는 이미지()와 설명문(

), 그리고 리액트 공식 사이트로 이동할 수 있는 링크()를 JSX 문법을 사용하여 화면에 표시하는 단순한 컴포넌트이다.

이제 App.test.js 파일을 열어 이 App 컴포넌트를 테스트하는 테스트 코드를 확인해 보자.

import { render, screen } from '@testing-library/react';
import App from './app';

test('renders learn react link', () => {
  render(<App />);
  const linkElement = screen.getByText(/learn react/i);
  expect(linkElement).toBeInTheDocument();
});

App.test.js 파일을 보면 @testing-library/react에서 render와 screen을 불러와 테스트에 사용하고 있음을 알 수 있다. 또한, Jest의 test 함수(it 함수와 같은 역할을 하는 함수)를 사용하여 테스트 명세를 작성한 테스트 코드임을 알 수 있다.

테스트 코드를 자세히 살펴보면 우선, @testing-library/react에서 불러온 render 함수는 리액트 컴포넌트를 화면에 표시하기 위함이고, screen은 리액트 컴포넌트가 표시된 화면을 의미한다. 여기서는 이해하기 쉽도록 표시된 화면이라고 표현했지만, 실제로는 화면에 표시되지는 않는다. 여기서 render 함수는 메모리상에 돔을 만들고 screen을 통해 해당 돔에 접근하는 것을 의미한다.

react-testing-library의 render 함수를 사용하여 App이라는 컴포넌트를 렌더링하였다. 이렇게 렌더링된 컴포넌트에서 screen.getByText를 통해 화면에서 'learn react'라는 글자를 가지고 있는 돔 요소(DOM Element)를 찾고 찾은 요소를 Jest의 expect().toBeInTheDocument()를 사용하여 돔에 표시되어 있는지 확인하였다.

이제 테스트 코드를 실행하기 위해 package.json 파일을 열어 스크립트의 내용을 확인해본다.

"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject",
},

create-react-app에서는 이미 test 스크립트를 기본적으로 지원하고 있음을 확인할 수 있다.
npm run test를 실행하여 Jest 테스트를 실행해 본다.

명령어를 실행하면 Jest가 App.test.js 파일의 테스트 코드를 실행하여 테스트를 수행하고 이전장과 동일한 결과를 표시하는 것을 알 수 있을 것이다.

이제 App.test.js 파일의 테스트 코드를 수정하여 react-testing-library의 사용법을 익혀보도록 하자. App 컴포넌트는 이미지와 설명문도 표시하고 있으므로 해당 이미지와 설명문이 잘 표시되었는지 확인하는 테스트 코드를 추가해 보도록 한다.

이미지와 설명문을 잘 표시하는지 확인하기 전에 테스트의 명세를 다음과 같이 수정한다.

describe('<App />', () => {
  it('renders component correctly', () => {
    render(<App />);
    const linkElement = screen.getByText(/learn react/i);
    expect(linkElement).toBeInTheDocument();
  });
});

앞의장에서 배운 Jest의 내용을 복습하기 위해 Jest의 describe 함수와 it 함수를 사용하였다. 또한, 테스트 명세 안에 링크 이외에 다른 항목도 테스트할 예정이므로 테스트 명세의 설명문을 알기 쉽게 수정하였다.

이미지가 잘 표시되는지 확인하기 위해 다음과 같이 테스트 코드를 수정한다.

describe('<App />', () => {
  it('renders component correctly', () => {
    const {container} = render(<App/>);
    expect(container.getElementsByClassName('App-logo')).toHaveLength(1);
    expect(container.getElementsByClassName('App-logo')[0]).toHaveAttribute(
        'src', 'logo.svg'
    )

    const linkElement = screen.getByText(/learn react/i);
    expect(linkElement).toBeInTheDocument();
  });
});

react-testing-library의 render 함수는 유용한 오브젝트들을 반환하는데 그중 하나인 container를 받아서 사용하였다. container는 리액트 컴포넌트에서 화면에 표시되는 부분을 담고있는 오브젝트이다. 이 container를 사용하여 getElementsByClassName 함수로 화면에 표시되고 있는 <img />를 클래스명으로 찾아가져 왔으며 가져온 HTML 요소 한개가 존재하는지를 toHaveLength 함수로 체크하였다.

또한, getElementsByClassName 함수를 통해 우리가 찾고자 하는 이미지가 존재하는지 확인하였고 해당 이미지가 실제로 우리가 원하는 이미지를 표시하고 있는지 toHaveAttribute 함수를 사용하여 <img/> 태그의 src를 가져와 비교하였다.

현재 npm run test로 Jest의 테스트가 계속 실행 중이므로 이렇게 App.test.js 파일을 수정한 후 저장하면 명령 프롬프트에 다음과 같은 결과가 표시되는 것을 확인할 수 있다.

Watch Usage: Press w to show more.
 PASS  src/App.test.js
  ✓ renders learn react link (30 ms)
  <App />
    ✓ renders component correctly (5 ms)

이제 화면에 설명문(<p/>)이 잘 표시되는지 확인해 보자. 화면에 설명문이 잘 표시되는지 확인하기 위해 테스트 명세를 다음과 같이 수정한다.

describe('<App />', () => {
  it('renders component correctly', () => {
    const {container} = render(<App/>);
    ...
    expect(container.getElementsByTagName('p')).toHaveLength(1);
    expect(container.getElementsByTagName('p')[0]).toHaveTextContent(
        'Edit src/App.js and save to reload.'
    );
  });
});

이번에는 container 오브젝트에서 getElementsByTagName 함수를 사용하여 <p> 태그를 찾고 해당 태그가 한 개 존재하고 있음을 toHaveLength 함수를 통해 테스트하였다. 또한, 해당 <p> 태그가 우리가 화면에 표시되길 원하는 문자열(Edit src/App.js and save to reload.)을 잘 표시하고 있는지 toHaveTestContent 함수를 사용하여 테스트하였다.

이제 App 컴포넌트의 모든 요소가 화면에 잘 표시되는 것을 테스트 코드를 통해 확인하였다. 마지막으로, 화면에 표시되는 내용이 변경되었는지 알기 위해 스냅샷 테스트를 추가한다.

describe('<App/>', () => {
  it('renders component correctly', () => {
    const { container } = render(<App />);
    ...
    expect(container).toMatchSnapshot();
  });
});

이렇게 파일을 수정하여 스냅샷 테스트를 추가한 후 저장하면 이전과는 다르게 다음과 같은 화면을 명령 프롬프트에서 확인할 수 있다.
image

또한, src/__snapshots__/App.test.js.snap이라는 파일이 생성된 것을 확인할 수 있다. 파일을 열어 내용을 확인해 보면 App 컴포넌트가 화면에 렌더링될 때 표시되는 HTML 내용이 저장된 것을 확인할 수 있다.

image

이렇게 저장된 스냅샷은 App 컴포넌트가 수정되어 화면에 표시되는 HTML 구조가 변경되면 에러를 표시하게 된다. 이렇게 스냅샷은 화면에 표시되는 컴포넌트가 변경되었는지 감지하기 위한 테스트로 많이 사용된다.

스냅샷 테스트가 제대로 동작하는지 확인하기 위해 App.js 파일을 열어 다음과 같이 수정한다.

만약 이 상태에서 스냅샷의 내용을 변경하면, 아래와 같이 테스트 코드가 실패하는 것을 확인할 수 있다.

image

이런 스냅샷 테스트는 우리가 리액트 컴포넌트를 수정했을 때, 수정한 내용이 의도치 않게 화면 표시를 변경하는 실수를 알 수 있게 해준다.

만약 컴포넌트를 수정하여 화면 표시가 변경된 것이 의도된 수정이었다면 스냅샷 테스트로 저장된 파일을 업데이트해 주어야 한다. 터미널에 위의 그림과 같은 에러가 표시된 상태에서 키보드의 'u' 키를 누르면 스냅샷으로 생성된 파일이 업데이트된다. 그러면 새롭게 업데이트된 스냅샷 파일이 다시 기준이 되어 변경을 감지하고 에러를 표시하게 된다.

728x90