본문으로 바로가기

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

category React 2021. 4. 23. 14:52

이전 포스팅에서 링크에 대한 테스트 코드를 작성했으니 이젠 폼에 대한 테스트를 작성할 차례다

포스트를 불러오고 화면에 제대로 그려지는지를 확인하는 과정을 거쳐야한다

테스트 코드 리팩토링

다음과 같이 빌드업해주면된다

describe('Subreddit form', ()=>{
    test('불러온 포스트가 화면에 그려진다',()=>{
        return render(
            <MemoryRouter>
                <App />
            </MemoryRouter>
        );
    })
})

가만 보니 이전에 링크를 테스트 할 때와 렌더링 부분이 똑같다

중복되는 코드를 막기 위해서 함수로 만든 뒤 간단하게 호출만 해주자

function setup() {
    return render(
        <MemoryRouter>
            <App />
        </MemoryRouter>
    );
}
describe('Subreddit form', ()=>{
    test('불러온 포스트가 화면에 그려진다',()=>{
        setup();
    })
})

header 테스트 코드들도 같이 바꿔주면 된다

폼을 변경하고 제출하기

폼의 인풋고 버튼은 각각 다음과 같이 접근할 수 있다

const input = screen.getByRole('textbox');
const button = screen.getByRole('button');

입력값을 변경하기 위해선 userEvent의 tpye을 사용하면된다

    test('불러온 포스트가 화면에 그려진다',()=>{
        setup();

        const input = screen.getByRole('textbox');
        const button = screen.getByRole('button');

        userEvent.type(input,'reactjs');

    })

이제 텍스트창에는 reactjs가 쳐져있을 것이고 다음으로는 폼을 click이벤트를 이용해 버튼을 눌러 폼을 전송해보자

    test('불러온 포스트가 화면에 그려진다',()=>{
        setup();

        const input = screen.getByRole('textbox');
        const button = screen.getByRole('button');

        userEvent.type(input,'reactjs');
        userEvent.click(button);

        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="reactjs"
                  />
                </label>
                <button
                  class="sc-AxmLO ckZYVg"
                  type="submit"
                >
                  Search
                </button>
              </form>
            </section>
            <div
              class="sc-AxheI hA-dxLm"
            >
              Is loading
            </div>
          </div>
        </main>
      </div>
    </body>

Is loading 중이라는 화면이 나타난 것을 볼 수 있다

포스트 목록이나 개수가 나와야하지 않냐는 의문을 가질 수도 있지만 버튼을 클릭하고나서 바로 돌려받는 화면은 Is loading이다

Is loading을 출력하고 있는 태그는 div로 아무런 aria role을 가지고 있지 않은데 어떻게 접근해야할까?

단순히 가지고 있는 텍스트로 dom에 접근하는 방식인 getByText을 사용하면된다

expect(screen.getByText(/Is loading/i)).toBeInTheDocument();

비동기 테스트하기

로딩화면을 테스트 했으니 이제 api요청이 끝난 후, 즉 데이터가 들어온 후의 화면을 테스트해야한다

getBy로 시작하는 모든 함수들은 동기적이기 때문에 이 함수들로는 데이터를 기다렸다 테스트할 수가 없다

대신 사용할 findBy함수는 검색 대상이 나타날 때 까지 기본적으로 5초를 기다린다

검색 대상을 설정하기 위해서는 데이터를 로드하고 나면 화면이 어떻게 바뀔지를 알아야하는데

이는 Home 컴포넌트를 살펴보면 알 수 있다

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

statue의 상태가 resolved가 되면 Number of top posts: ...의 형태로 나타나는 것을 볼 수 있다

findBy 함수들은 비동기이니 await를 붙여주고 test의 콜백에는 async를 붙여주자

   test('불러온 포스트가 화면에 그려진다', async ()=>{
        setup();

        const input = screen.getByRole('textbox');
        userEvent.type(input,'reactjs');

        const button = screen.getByRole('button');
        userEvent.click(button);

        expect(screen.getByText(/Is loading/i)).toBeInTheDocument();

        const numberOfPosts = await screen.findByText(/Number of top posts:/i);

        screen.debug(numberOfPosts);

    })

api 위조하기

위에서 버튼을 클릭하고 데이터를 받아오는게 아무 문제 없이 실행되었지만 테스트환경에서 진짜 api에 요청을 보내는 것은 현명하지못하다

  1. API 요청은 시간이 많이 걸린다

테스트 케이스마다 API에 요청을 하게되면 효율성이 떨어진다

  1. API는 우리가 컨트롤 할 수 없다

다양한 환경에서 테스트해야하지만 API 서버는 우리가 컨트롤 할 수가 없다

끄고 싶을 때 끌 수도 없고 심지어 성공 케이스를 테스트하고 싶어도 API서버가 닫혀있다면 테스트할 수 없다

그래서 json을 미리 만들어놓고 진짜 API대신 만들어 놓은 json을 요청하는 방법으로 하면된다

우선 라이브러리가 필요한데 jset-fetch-mock을 설치하고 테스트 파일 상단에 초기화를 해주자

import fetchMock from 'jest-fetch-mock';

fetchMock.enableMocks();

이렇게 fetchMock을 초기화해주면 테스트 코드 내에서 비동기 요청을 가로채어간다

여기까지만 하고 테스트를 돌려보면 가로채가고 가짜 데이터를 던져주지 않았으니 원래 성공했던 테스트가 실패하는 것을 볼 수 있다

그럼 가짜 데이터를 주기위해 가짜 데이터를 만들자

실제 서버를 실행시켜서 reactjs를 입력한 후에 폼을 전송한 뒤 정상적으로 화면이 출력됐다면 개발자 도구의 Network탭에 가서 응답으로 돌려준 json파일을 카피한다

그리고 /src/__mock__ 하위에 응답용 json파일을 만든 뒤 위에서 카피한 데이터를 붙여 넣으면된다

이제 다음과 같이 테스트 하면 된다

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';
import fetchMock from 'jest-fetch-mock';
import mockResponse from './__mocks__/subreddit-reactjs-response.json';

fetchMock.enableMocks();

function setup() {
    return render(
        <MemoryRouter>
            <App />
        </MemoryRouter>
    );
}
...
describe('Subreddit form', ()=>{
    test('불러온 포스트가 화면에 그려진다', async ()=>{
        fetch.once(JSON.stringify(mockResponse));
        setup();

        const input = screen.getByRole('textbox');
        userEvent.type(input,'reactjs');

        const button = screen.getByRole('button');
        userEvent.click(button);

        expect(screen.getByText(/Is loading/i)).toBeInTheDocument();

        const numberOfPosts = await screen.findByText(/Number of top posts:/i);
        expect(numberOfPosts).toBeInTheDocument();
    })
})

이제는 들어오는 데이터가 고정되어있기 때문에 정규표현식에 number of top posts: 25로 검색해도 된다

한 가지 더 신경써주어야하는데 지금 어떤 API에 무슨 요청을 해도 만들어둔 mockResponse 데이터가 들어온다

그래서 제대로된 API에 요청을 했는지 그것도 테스트해주어야한다

expect(fetch).toHaveBeenCalledWith('https://www.reddit.com/r/reactjs/top.json')로 손쉽게 테스트해 줄 수 있다