본문 바로가기

FRONTEND

[React] React Testing Library(RTL) 활용한 테스트 코드 작성 - Part 1

해당 글은 ‘An in-depth beginner's guide to testing React applications in 2020’(https://jkettmann.com/beginners-guide-to-testing-react)의 번역된 내용을 기반으로 작성되었다.

직접해보고 싶다면 해당 레포지토리에서 받아 해보자 (해당 코드 깃 레포지토리)

 

들어가며..

이번 팀프로젝트를 진행하면서 처음으로 React Testing Library를 이용한 테스트 사용하여 테스트 코드를 작성해보았다.

테스트 코드를 작성하면서 무엇인가 정리되지 않는 느낌을 받아 기초부터 이해하며 사용하고 싶다는 생각이 들었다.

 

테스트 코드를 처음 작성하는 경우, 상당한 어려움을 겪는다. 테스트코드를 작성하는 것은 기존에 코드를 작성하는 것과는 달리 완전 새로운 환경에서 개발을 하는 느낌을 주기 때문이다. 나도 또한 테스트 코드를 작성해보면서 단 몇줄의 테스트 코드를 작성하는데 상당한 시간이 걸렸고 돌아보니 테스트 코드에 대한 내용이 머리속에 남지않았을 때 허무함을 느꼈다.

 

내가 겪은 가장 큰 어려움은 '무엇을 테스트 할것인가?' 에 대한 것이었다.

어떤 컴포넌트에 어떤 테스트를 작성해야하는지 또 그 종류는 무엇이 있는지를 정확히 파악하지 못했다.

 

정리하는 이 과정을 통해 이를 해소하고 정확히 이해하고 사용하기를 희망한다.

 

테스트하는 목적은 무엇일까?

개발하는 어플리케이션 또는 웹의 규모가 커지면 커질수록 핵심 기능들에 대한 코드를 건드리기가 무섭다. 아마 작은 변경이 핵심 기능에 큰 문제를 가져다줄 수 있기 때문이다 이럴 경우에 테스트 코드가 빛을 발휘한다.

 

개발한 어플리케이션이 올바르게 작동하는지를 테스트 코드를 이용해 확인하며 작업할 수 있다는 것에 그 목적이 있다.

 

React Testing Library

테스트에 사용되는 라이브러리는 다양하다. 하지만 보편적으로 Jest에 React Testing Library를 얹어 사용한다.

Enzyme 테스팅 라이브러리도 많이 사용한다.

 

출처: npm trends enzyme vs react-testing-library

 

React Testing Library는 사용자의 관점 에서 테스트를 진행한다. 예를 들어, 어떤 기능을 수행하는 버튼이 있다고 하자. React Testing Library는 그 버튼을 클릭했을때 발생하는 특정한 이벤트(효과)를 테스트 한다.(삭제하기 버튼을 눌렀을때 재확인 하는 모달창을 띄우는 것과 같은)

 

이런 관점에서 React Testing Library를 사용하여 여러개의 컴포넌트들을 테스트 할 수 있는 통합테스트(intergration tests)를 진행할 수 있다. Enzyme 테스팅 라이브러리를 사용하면 좀 더 구체적인 테스트를 진행할 수 있는데 해당 이벤트 핸들러가 제대로 실행되었는지 또 해당 상태(state)가 제대로 업데이트 되었는지 등을 테스트 할 수 있다.

 

테스트 해보기(사전 작업)

해당 글에서는 subreddit에 가장 많이 게시가 된 글을 찾아주는 간단한 앱을 이용하여 테스트를 진행할 것이다.

 

 

해당 웹 어플리케이션 상단에는 로고, 다른 페이지로 이동하는 두 개의 링크로 구성되어 있다. 여기서 중요한 것은 이들이 네비게이터의 역할을 한다는 것이다. 그 밑에는 form으로 이루어진 input, button이 있다. 입력창에 사용자는 검색어를 입력할 수 있고 버튼을 누르면 실제로 Reddit API에 요청을 보내도록 되어있다. 또한 API 요청을 보내는 동안 화면에는 잠깐이지만 로딩 안내화면이 나타난다. API로 부터 응답을 받으면 화면에 해당 검색어와 관련된 상위 게시물의 숫자를 보여준다. 

 

무엇을 테스트 할까?

그렇다면 해당 앱에서 무엇을 테스트 해야할까?

우선 Form 컴포넌트를 살펴보자!

function Form({ onSearch }) {
  const [subreddit, setSubreddit] = useState('javascript');

  const onSubmit = (event) => {
    event.preventDefault();
    onSearch(subreddit);
  };

  return (
    <FormContainer onSubmit={onSubmit}>
      <Label>
        r /
        <Input type="text" name="subreddit" value={subreddit} onChange={(event) => setSubreddit(event.target.value)} />
      </Label>
      <Button type="submit">Search</Button>
    </FormContainer>
  );
}

form 태그의 input 태그의 value, onChange 속성을 통해 입력창에 입력되는 값을 subreddit 이라는 상태로 관리하고 있다. 검색어를 입력후 Search 버튼을 클릭했을때, 부모 컴포넌트로부터 전달받은 onSearch 이벤트 핸들러가 실행된다.

 

다음으로, 데이터 요청에 대한 코드를 살펴보자 해당 로직은 Home 컴포넌트에서 작성된 로직이다.

function Home() {
  const [posts, setPosts] = useState([]);
  const [status, setStatus] = useState('idle');

  const onSearch = async (subreddit) => {
    setStatus('loading');
    const url = `https://www.reddit.com/r/${subreddit}/top.json`;
    const response = await fetch(url);
    const { data } = await response.json();
    setPosts(data.children);
    setStatus('resolved');
  };

  return (
    <Container>
      <Section>
        <Headline>Find the best time for a subreddit</Headline>
        <Form onSearch={onSearch} />
      </Section>
      {status === 'loading' && <Status>Is loading</Status>}
      {status === 'resolved' && <TopPosts>Number of top posts: {posts.length}</TopPosts>}
    </Container>
  );
}

Home 컴포넌트 API 응답의 결과를 posts 상태에 관리하고 있고 비동기 요청의 시작과 끝에 status 상태를 이용하여 로딩에 대한 처리를 하고있다. Search 버튼을 클릭했을 때, Reddit API로 요청을 보내고 요청에 대한 응답으로 데이터를 전송 받으면 두 상태가 업데이트 되며 화면에 나타나게(렌더링) 된다.

 

해당 어플리케이션의 핵심적인 구조를 살펴보았으니 스스로에게 질문을 던져보자


“Form, Home 이 두개의 컴포넌트를 어떻게 테스트 할 수 있을까?”

 

 

기본적으로 각 컴포넌트의 상태가 적절하게 관리되고 있고 Form 컴포넌트의 onSearch 프로퍼티(부모로부터 전달받은)가 입력창에 입력된 값에 따라 적절하게 실행되고 있는지에 대해 테스트를 진행하고 싶을 것이다.  하지만 이와같은 테스트는 Enzyme 테스팅 라이브러리를 사용해 테스트 코드를 작성할 수 있다. 다시말해, React Testing Library를 통해서 상태들이 적절한 값으로 업데이트 되었는지는 테스트할 수 없다는 뜻이다.

 

Form 컴포넌트의 상태를 부모 컴포넌트로 전달할 수 있다는 사실이 중요하고 어플리케이션이 동일하게 작동한다.(수정필요) React Testing Library를 통해 코드에 자체에 집중하는 것이 아닌 코드가 어떻게 작동하는지 '사용자의 관점'에서 바라볼 수 있다는 것이 중요하다. 이러한 특징이 어플리케이션의 핵심에 대한 테스트를 가능하게 한다.

 

잠시 컴포넌트에 대한 생각은 접어두고 사용자의 입장에서 해당 어플리케이션을 바라보자. 사용자의 관점을 고려했을때, 어플리케이션이 올바르게 작동하기 위해서는 어떤 부분이 필요할까? 해당 어플리케이션을 직접 사용하며 생각해보자.(이것저것 클릭! 해보자)

 

  • 첫번째, 사용자가 입력창에 검색어를 입력하고 Search 버튼을 눌러 해당 검색어에 대한 데이터를 요청한다.
  • 두번째, 데이터에 대한 응답을 기다리는 동안 로딩 메세지를 보여준다.
  • 세번째, 응답이 왔을 때, 해당 데이터를 이용해 화면에 내용을 보여준다.

 

사용자의 입장에서 생각해보면, 내부적으로 Home 컴포넌트와 Form 컴포넌트에서 입력값에 대한 상태를 어떻게 관리하는지는 중요한 내용이 아니다. 오히려 위에 나열한 3개의 내용이 사용자에게 있어서 더욱 중요한 부분이다.

 

테스트코드 작성해보기

테스트 해야할 내용들을 다시 정리해보자.

 

첫째, 헤더 영역에 있는 링크에 대한 테스트

  • 해당 링크를 클릭했을 때 올바르게 이동하는지

 

둘째, 컴포넌트에 대한 테스트

  • Form 컴포넌트의 입력창에 검색어를 입력하고 검색하는 과정
  • 로딩 화면
  • 검색의 결과가 화면에 적절하게 나타나는지

 

먼저, 헤더 영역의 링크에 대한 테스트를 작성해보자

src/App.test.js 파일을 열고 안에 있는 내용들을 전부 지우고 시작하자.

 

테스트 코드의 시작은 아래와 같다.

test를 describe로 반드시 감싸줄 필요는 없다. describe 를 통해 비슷한 성격의 테스트 끼리 묶어주자 또한 test 키워드 대신 it 을 사용해도 된다(두가지 모두 Jest에서 제공하는 기능이다)

describe('Header', () => {
  test('"How it works" link points to the correct page', () => {});
});

 

앞서, React Testing Library는 컴포넌트를 독립적으로 테스트하기 보다는 어플리케이션의 전체적인 동작 관점에서 테스트를 진행한다고 했다. 따라서 테스트를 위해 Header 컴포넌트가 아닌 App 컴포넌트를 사용해야한다. (해당 부분의 기존의 코드는 react-router-dom v5로 작성되어있어 v6 로 변경하여 작성하였다)

import React from 'react';
import { Routes, Route } from 'react-router-dom';
import GlobalStyle from './GlobalStyle';
import Header from './components/Header';
import Home from './pages/Home';

function App() {
  return (
    <>
      <GlobalStyle />
      <Header />
      <main>
        <Routes>
          <Route path="/how-it-works" element={<h1>How it works</h1>} />
          <Route path="/about" element={<h1>About</h1>} />
          <Route path="/" element={<Home/>} />
        </Routes>
      </main>
    </>
  );
}

 

 

Part 2에서 계속