본문으로 바로가기

리액트 테스트 코드 작성하기 1

category React 2021. 4. 22. 13:09

리액트 라이브러리

리액트 환경에서 테스트 코드를 작성하는데 사용하는 라이브러리는 크게 두 가지가 존재한다

  1. enzyme

  2. testing-library

무엇에 중점을 두고 테스트하냐로 서로의 기능이 조금씩 다른데

enzyme 같은 경우에는 컴포넌트 내부의 props, state, 내장 메소드 등, 일반적인 유닛테스트와 비슷하다

반면 testing-library는 컴포넌트 내부의 데이터가 어떻게 변화했는지, 잘 변화했는지를 살펴보기 보다는 화면에만 집중한다

화면에 무엇이 보이는지, 이벤트가 발생했을 때 성공적인 화면이 보여지고 있는지 사용자의 입장에서 보여지는 view가 성공적인지만 테스트하게된다

테스트 코드를 연습할 코드

여기서 소스를 가져다가 썼다

레딧에서 가장 인기있는 포스트를 찾을 수 있도록 도와주는 앱으로 입력 폼, 검색 버튼, 적은 페이지 링크 를 가지고 있다

입력 폼에 텍스트를 입력하고 검색 버튼을 누르면 Reddit API에 요청을 보내어서 상위 포스트를 받아온다

이게 전부다

무엇을 테스트해야할까

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>
  );
}

폼에 입력되는 벨류 subreddit을 submit하면 상위 컴포넌트에서 받아온 onSearch에 subreddit을 전달해준다

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 top posts on Reddit
        </Headline>

        <Form onSearch={onSearch} />
      </Section>

      {
        status === 'loading' && (
          <Status>
            Is loading
          </Status>
        )
      }
      {
        status === 'resolved' && (
          <TopPosts>
            Number of top posts: {posts.length}
          </TopPosts>
        )
      }
    </Container>
  );
}

api를 요청하면서 status를 loading으로

요청이 끝나면 status를 resloved로 바꾸면서 불러온 포스트의 개수를 사용자에게 전달하고 있다

이 두 컴포넌트를 어떻게 테스트해야할까..?

유닛 테스트가 익숙한 사람이라면 subreddit의 값과 onSearch가 제대로 호출되는지 확인하려고 할 것이다

실제로 이런 방향은 enzyme로 테스트하는 개발자들이 자주 사용하는 방법이다

하지만 우리는 testing-library로 뷰를 중심으로 테스트할 것이기 때문에 상태변수에는 접근하지 않는다

사용자의 관점에서

뷰에 집중하는 테스트를 만들기위해선 어떻게 생각해야할까

바로 사용자의 입장에서 앱을 테스트해보면되는데

  1. 사용자는 폼에 데이터를 입력하고 검색 버튼을 누른다

  2. 검색 요청 중엔 loading화면이 나온다

  3. 검색 요청이 끝나면 데이터가 화면에 나온다

유저에게 중요한 것은 위의 3단계다

폼 컴포넌트데 입력값을 저장하는지 데이터가 제대로 들어오는지는 상관할 바가 아니다

테스트 작성

두 개의 묶음을 작성할 건데

하나는 헤더, 하나는 폼을 위한 테스트이다

헤더에 존재하는 네비게이터들의 링크를 클릭했을 때 정확한 대상을 가르키고 있는지 확인하기 위한 테스트이다

우리는 이 테스트를 헤더 컴포넌트 하나의 상태만 테스트하는 것이 아닌 앱 컨텍스트에서 테스트할 것이기 때문에 다음과 같이 App을 렌더링한다

describe('Header', () => {
    test('"How it works" 를 클릭했을 때 정확한 페이지를 가르킨다', ()=>{
        render(
            <MemoryRouter>
                <App/>
            </MemoryRouter>
        )
    })
});

MemoryRouter로 감싼 것을 볼 수 있는데 이는 Router와 같은 역할을 하지만 주소 표시줄에 url을 나타내지 않는 대신 메모리에 기록한다

리액트 라우터에 관한 테스트 코드를 짤 때 주로 볼 수 있는 패턴이다

디버깅

프론트에서 화면을 개발하다가 화면이 잘 나오는지 확인하려면 어떻게 하는가?

브라우저를 열어서 개발자 도구를 열어 DOM을 확인하거나 로그를 확인할 것이다

하지만 테스트 코드를 작성하다 원하는 결과를 얻을 수 없다면 어떻게 확인할 것인가?

그런 경우에 테스트 코드가 돌아간 상태의 DOM을 확인할 수 있게 해주는 디버깅 함수 screen.debug가 존재한다

위에서 render한 화면이 screen객체에 담겨있고 그를 로그로 출력하게 하는 것

import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
import { MemoryRouter } from 'react-router-dom';

describe('Header', () => {
    test('"How it works" 를 클릭했을 때 정확한 페이지를 가르킨다', ()=>{
        render(
            <MemoryRouter>
                <App/>
            </MemoryRouter>
        )
        screen.debug();
    })
});

이 상태로 테스트를 돌리면

    <body>
      <div>
        <header
          class="sc-AxjAm hXjNkZ"
        >
          <a
            href="/"
          >
            <svg
              class="sc-AxirZ iCeOHL"
            >
              logo.svg
            </svg>
          </a>
          <nav>
            <a
              class="sc-AxiKw ejNrOs"
              href="/how-it-works"
            >
              How it works
            </a>
            <a
              class="sc-AxiKw ejNrOs"
              href="/about"
            >
              About
            </a>
          </nav>
        </header>
        <main>
          <div
            class="sc-AxhCb iQZeXb"
          >
            <section
              class="sc-AxhUy bmKebj"
            >
              <h1
                class="sc-AxgMl hHcRn"
              >
                Find the top posts on Reddit
              </h1>
              <form
                class="sc-fzozJi gsgKPd"
              >
                <label
                  class="sc-fzoLsD gcKKKR"
                >
                  r /
                  <input
                    class="sc-fzpans hAQcze"
                    name="subreddit"
                    type="text"
                    value="javascript"
                  />
                </label>
                <button
                  class="sc-AxmLO ckZYVg"
                  type="submit"
                >
                  Search
                </button>
              </form>
            </section>
          </div>
        </main>
      </div>
    </body>

DOM을 얻을 수 있고 그 중에 우리가 테스트하려고 하는 How it works의 링크가 포함된 것을 볼 수 있다

DOM에 접근하는 법

이제 screen객체에 DOM이 제대로 담겨있는 것을 봤으니 우리가 원하는 요소를 비교해서 테스트해야하는데

이를 위해 screen객체에서 다양한 쿼리들을 제공한다 그리고 testing-library 문서에는 어떤 상황에서 어떤 것을 써야하는지 가르쳐주고 있다

여기서는 getByRole을 사용한다

getByRole는 첫 번째 요소로 대상 요소의 속성이어야한다

여기서는 link를 사용할 수 있는데 App 내부에는 link가 다수 존재하기 때문에 다음과 같이 옵션을 주어야한다

const link = screen.getByRole('link',{name:/How it works/i});

정규표현식을 사용해서 원하는 값을 찾을 수 있다

값을 찾았다면 테스트를 성공, 찾지 못했다면 실패하게 되고 찾아온 값을 확인하고 싶다면 아까와 같이 debug함수를 이용해 결과값을 볼 수 있다

screen.debug(link)

    <a
      class="sc-AxiKw ejNrOs"
      href="/how-it-works"
    >
      How it works
    </a>

DOM과 상호작용

우리가 원하는 링크를 찾았지만 이게 끝은 아니다 우리는 링크를 클릭해서 우리가 원하는 곳을 가르키고 있는지를 테스트해야한다

그러기 위해선 주소를 클릭해야하는데 이러한 이벤트 또한 테스트 내부에서 실행시킬 수 있다

테스팅 라이브러리 내부에 모든 이벤트가 준비되어있다

import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
import { MemoryRouter } from 'react-router-dom';
import userEvent from "@testing-library/user-event/src";

describe('Header', () => {
    test('"How it works" 를 클릭했을 때 정확한 페이지를 가르킨다', ()=>{
        render(
            <MemoryRouter>
                <App/>
            </MemoryRouter>
        )
        const link = screen.getByRole('link',{name:/How it works/i});

        userEvent.click(link);
    })
});

올바른지 확인하기

이제 클릭해서 이동했으니 이동한 페이지가 올바른지 확인할 차례다

그 방법 중 하나는 url을 확인하는 방법인데

사실 유저는 url을 확인하지 않는다.. 그리고 url이 옳다고 해도 404에러가 발생할 수 있다

제대로 확인하는 방법은 유저가 보고있는 화면이 제대로 떠 있는가 이다

페이지 구현 부분을 보면

<Route path="/how-it-works">
<h1>How it works</h1>

로 제목이 How it works인 것을 알 수 있는데 현재 페이지에서 제목이 How it works인지 확인하면 된다

expect(
    screen.getByRole('heading', { name: /how it works/i })
).toBeInTheDocument();

이제 테스트는 통과할 수 있다

'React' 카테고리의 다른 글

html태그가 포함된 문자열 렌더링하기  (0) 2021.04.29
리액트 테스트 코드 작성하기 2  (0) 2021.04.23
리액트 프로젝트 설치 종속성 에러  (0) 2021.04.20
Styled-components  (0) 2021.02.26
Redux-saga  (0) 2021.02.25