공부하는 블로그

React Testing Library 튜토리얼 본문

React

React Testing Library 튜토리얼

devtimothy 2020. 11. 1. 18:13

React Testing Library 튜토리얼

이 내용은 이 곳의 내용을 번역한 것입니다.

Kent C. Dodds의 React Testing Library (React Testing Library)이 Airbnb의 Enzyme의 대안으로 출시되었습니다. Enzyme은 React 개발자에게 React 컴포넌트의 내부를 테스트 할 수있는 유틸리티를 제공하지만 React Testing Library는 한 걸음 물러서서 "React 컴포넌트를 완전히 신뢰하기 위해 React 컴포넌트를 테스트하는 방법"에 대해 질문합니다. 컴포넌트의 구현 세부 정보를 테스트하는 대신 React Testing Library 개발자를 React 애플리케이션의 최종 사용자의 입장에서 볼 수 있습니다.

이 React Testing Library 자습서에서는 React 컴포넌트를 단위 테스트 및 통합 테스트하는 데 필요한 모든 단계를 자신있게 살펴볼 것입니다.

Jest vs React Testing Library

React 초보자는 종종 React에서 테스트하기위한 도구를 혼동합니다. React Testing Library는 Jest의 대안이 아닙니다. 서로를 필요로하고 그들 모두가 명확한 작업을 가지고 있기 때문입니다.

현대 React에서 개발자는 Jest를 테스트하기 위해 사용하지 않을 것입니다. JavaScript 애플리케이션을 위한 가장 인기있는 테스트 프레임 워크이기 때문입니다. 테스트 스크립트를 사용하여 package.json을 설정하면 실행할 수있는 테스트 러너 외에도 Jest는 테스트를 위해 다음 기능을 제공합니다.

describe('my function or component', () => {
  test('does the following', () => {

  });
});

describe-block이 테스트 스위트 인 반면, test-block (대신 이름을 지정할 수도 있음)은 테스트 케이스입니다. 테스트 스위트에는 여러 테스트 케이스가있을 수 있으며 테스트 케이스는 테스트 스위트에 있을 필요가 없습니다. 테스트 케이스에 넣은 것을 어설션 (예 : Jest에서)이라고하며 성공 (녹색) 또는 오류 (빨간색)로 판명됩니다. 여기에 성공해야하는 두 가지 assertion이 있습니다.

describe('true is truthy and false is falsy', () => {
  test('true is truthy', () => {
    expect(true).toBe(true);
  });

  test('false is falsy', () => {
    expect(false).toBe(false);
  });
});

이 테스트 스위트와 assertion이 포함 된 테스트 케이스를 test.js 파일에 넣으면 Jest를 실행할 때 자동으로 선택합니다. 테스트 명령을 실행할 때 Jest의 테스트 실행기는 기본적으로 test.js 접미사가있는 모든 파일을 일치시킵니다. 맞춤 Jest 구성 파일에서 일치하는 패턴 및 기타 사항을 구성 할 수 있습니다.

create-react-app을 사용하는 경우 Jest (및 React Testing Library)가 기본적으로 설치와 함께 제공됩니다. 커스텀 React 설정을 사용하는 경우 Jest (및 React Testing Library)를 직접 설치하고 설정해야합니다.

Jest의 테스트 실행기를 통해 (또는 package.json에서 사용중인 스크립트) 테스트를 실행하면 이전에 정의 된 두 테스트에 대해 다음 출력이 표시됩니다.

 PASS  src/App.test.js
  true is truthy and false is falsy
    ✓ true is truthy (3ms)
    ✓ false is falsy

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        2.999s
Ran all test suites related to changed files.

Watch Usage
 › Press a to run all tests.
 › Press f to run only failed tests.
 › Press q to quit watch mode.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press Enter to trigger a test run.

케이스에 대해 녹색으로 변해야하는 모든 테스트를 실행 한 후 Jest는 추가 지침을 제공 할 수있는 대화 형 인터페이스를 제공합니다. 그러나 종종 모든 테스트에 대해 녹색으로 변해야하는 하나의 테스트 출력 일뿐입니다. 소스 코드 든 테스트 든 파일을 변경하는 경우 Jest는 모든 테스트를 다시 실행합니다.

function sum(x, y) {
  return x + y;
}

describe('sum', () => {
  test('sums up two values', () => {
    expect(sum(2, 4)).toBe(6);
  });
});

실제 JavaScript 프로젝트에서 테스트하려는 함수는 다른 파일에있는 반면 테스트는 테스트 할 함수를 가져 오는 테스트 파일에 있습니다.

import sum from './math.js';

describe('sum', () => {
  test('sums up two values', () => {
    expect(sum(2, 4)).toBe(6);
  });
});

본질적으로 그것은 Jest입니다. 아직 React 컴포넌트에 대한 것은 없습니다. Jest는 명령 줄에서 Jest로 테스트를 실행할 수있는 기능을 제공하는 테스트 실행기입니다. 또한 Jest는 테스트 스위트, 테스트 케이스 및 assertion을위한 기능을 제공합니다. 물론 프레임 워크는 이보다 더 많은 것을 제공합니다 (예 : 스파이, 모의, 스텁 등). 하지만 본질적으로 그것이 우리가 애초에 Jest가 필요한 이유를 이해하는 데 필요한 모든 것입니다.

Jest와 달리 React Testing Library는 React 컴포넌트를 테스트하는 테스트 라이브러리 중 하나입니다. 이 범주에서 또 다른 인기있는 것은 앞서 언급 한 효소입니다. 다음 섹션에서는 React 컴포넌트를 테스트하기 위해 React Testing Library를 사용하는 방법을 볼 것입니다.

테스트 라이브러리 리액트: 컴포넌트 렌더링

create-react-app을 사용하는 경우 기본적으로 React Testing Library가 있습니다. 커스텀 React 설정 (예 : React with Webpack) 또는 다른 React 프레임 워크를 사용하는 경우 직접 설치해야합니다. 이 섹션에서는 React Testing Library로 테스트에서 React 컴포넌트를 렌더링하는 방법을 배웁니다. src / App.js 파일에서 다음 앱 함수 컴포넌트를 사용합니다.

import React from 'react';

const title = 'Hello React';

function App() {
  return <div>{title}</div>;
}

export default App;

그리고 src/App.test.js 파일에서 테스트 :

import React from 'react';
import { render } from '@testing-library/react';

import App from './App';

describe('App', () => {
  test('renders App component', () => {
    render(<App />);
  });
});

React Testing Library의 렌더링 함수는 모든 JSX를 사용하여 렌더링합니다. 나중에 테스트에서 React 컴포넌트에 액세스 할 수 있어야합니다. 그것이 있다는 것을 확신하기 위해 React Testing Library의 디버그 기능을 사용할 수 있습니다 :

import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () => {
  test('renders App component', () => {
    render(<App />);

    screen.debug();
  });
});

명령 줄에서 테스트를 실행 한 후 앱 컴포넌트의 HTML 출력이 표시되어야합니다. React Testing 라이브러리를 사용하여 컴포넌트에 대한 테스트를 작성할 때마다 컴포넌트를 먼저 렌더링 한 다음 테스트에서 React Testing Library의 렌더러에 표시되는 내용을 디버깅 할 수 있습니다. 이렇게 하면 보다 자신있게 테스트를 작성할 수 있습니다.

<body>
  <div>
    <div>
      Hello React
    </div>
  </div>
</body>

그것에 대한 좋은 점은 React Testing Library는 실제 컴포넌트에별로 신경 쓰지 않는다는 것입니다. 다양한 React 기능 (useState, 이벤트 핸들러, props)과 개념 (제어 된 컴포넌트)을 활용하는 다음 React 컴포넌트를 살펴 보겠습니다.

import React from 'react';

function App() {
  const [search, setSearch] = React.useState('');

  function handleChange(event) {
    setSearch(event.target.value);
  }

  return (
    <div>
      <Search value={search} onChange={handleChange}>
        Search:
      </Search>

      <p>Searches for {search ? search : '...'}</p>
    </div>
  );
}

function Search({ value, onChange, children }) {
  return (
    <div>
      <label htmlFor="search">{children}</label>
      <input
        id="search"
        type="text"
        value={value}
        onChange={onChange}
      />
    </div>
  );
}

export default App;

앱 컴포넌트의 테스트를 다시 시작하면 디버그 함수에서 다음 출력이 표시되어야합니다.

<body>
  <div>
    <div>
      <div>
        <label
          for="search"
        >
          Search:
        </label>
        <input
          id="search"
          type="text"
          value=""
        />
      </div>
      <p>
        Searches for
        ...
      </p>
    </div>
  </div>
</body>

React Testing Library는 사람처럼 React 컴포넌트와 상호 작용하는 데 사용됩니다. 사람이 보는 것은 React 컴포넌트에서 HTML로 렌더링 된 것이므로이 HTML 구조를 두 개의 개별 React 컴포넌트가 아닌 출력으로 보는 것입니다.

테스트 라이브러리 리액트: 엘리먼트 선택

React 컴포넌트를 렌더링한 후 리액트 테스트 라이브러리는 엘리먼트를 캡처할 수 있는 다양한 검색 기능을 제공합니다. 그런 다음 이러한 엘리먼트는 어설션이나 사용자 상호 작용에 사용됩니다. 그러나 우리가 이런 일을 하기 전에, 그들을 찾는 방법에 대해 알아봅시다.

import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () => {
  test('renders App component', () => {
    render(<App />);

    screen.getByText('Search:');
  });
});

React Testing Library의 렌더링 함수의 렌더링 된 출력이 무엇인지 잘 모르는 경우 항상 React Testing Library의 디버그 함수를 사용하십시오. HTML 구조에 대해 알고 나면 React Testing Library의 화면 개체 기능을 사용하여 엘리먼트를 선택할 수 있습니다. 그런 다음 선택한 엘리먼트를 사용자 상호 작용 또는 assertion에 사용할 수 있습니다. 엘리먼트가 DOM에 있는지 확인하는 assertion을 수행합니다.

import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () => {
  test('renders App component', () => {
    render(<App />);

    expect(screen.getByText('Search:')).toBeInTheDocument();
  });
});

엘리먼트를 찾을 수없는 경우 기본적으로 편리하게 오류를 발생시킵니다. 이것은 선택한 엘리먼트가 처음에 존재하지 않는다는 테스트를 작성하는 동안 힌트를 제공하는 데 유용합니다. 몇몇 사람들은 이 동작을 이용하여 : getByTextgetByTextexpect를 사용한 명시 적 assertion 대신 암시 적 assertion 대체와 같은 검색 기능을 사용합니다.

import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () => {
  test('renders App component', () => {
    render(<App />);

    // implicit assertion
    // because getByText would throw error
    // if element wouldn't be there
    screen.getByText('Search:');

    // explicit assertion
    // recommended
    expect(screen.getByText('Search:')).toBeInTheDocument();
  });
});

이 함수는 우리가 지금 사용하고있는 문자열을 입력으로 받아들이지 만 정규 표현식도 받습니다. 정확한 일치를 위해 문자열 인수가 사용되는 반면, 부분 일치에는 정규 표현식을 사용할 수 있습니다.

import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () => {
  test('renders App component', () => {
    render(<App />);

    // fails
    expect(screen.getByText('Search')).toBeInTheDocument();

    // succeeds
    expect(screen.getByText('Search:')).toBeInTheDocument();

    // succeeds
    expect(screen.getByText(/Search/)).toBeInTheDocument();
  });
});

이 기능은 React Testing Library에있는 여러 유형의 검색 기능 중 하나 일뿐입니다. 거기에 무엇이 있는지 봅시다.

테스트 라이브러리 리액트: 검색 타입

Text 가 여러 검색 타입 중 하나라는 것을 대해 배웠습니다. Text는 종종 React Testing Library로 엘리먼트를 선택하는 일반적인 방법이지만, 또 다른 강점은 getByText, getByRole을 사용한 role입니다.

이 함수는 일반적으로 aria-label 속성으로 엘리먼트를 검색하는 데 사용됩니다. 그러나 버튼 엘리먼트의 버튼과 같이 HTML 엘리먼트에 대한 암시적 role도 있습니다. 따라서 표시되는 텍스트뿐만 아니라 React Testing Library를 통한 접근성 role로 엘리먼트를 선택할 수 있습니다. 이것의 깔끔한 기능은 사용할 수없는 role을 나타내면 role을 제안한다는 것입니다. 둘 다 React Testing Library에서 가장 널리 사용되는 검색 함수입니다 . getByText, getByRole

깔끔한 점 : 렌더링 된 컴포넌트의 HTML에서 사용할 수없는 role을 제공하면 선택 가능한 모든 role이 표시됩니다.

import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () => {
  test('renders App component', () => {
    render(<App />);

    screen.getByRole('');
  });
});

즉, 이전 테스트는 실행 한 후 명령줄에 다음을 출력합니다.

Unable to find an accessible element with the role ""

Here are the accessible roles:

document:

Name "":
<body />

--------------------------------------------------
textbox:

Name "Search:":
<input
  id="search"
  type="text"
  value=""
/>

--------------------------------------------------

HTML 엘리먼트의 암시적 role 때문에 이 검색 타입으로 검색할 수 있는 최소한 텍스트 상자(여기) 엘리먼트가 있습니다.<input />

import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () => {
  test('renders App component', () => {
    render(<App />);

    expect(screen.getByRole('textbox')).toBeInTheDocument();
  });
});

DOM에 이미 HTML 엘리먼트에 암시적 role이 첨부되어 있기 때문에 테스트를 위해 HTML 엘리먼트에 aria-role을 명시적으로 할당할 필요가 없습니다. 이것이 리액트 테스트 라이브러리에서 검색 기능에 강력한 경쟁력을 만드는 이유입니다.

엘리먼트에 특정한 다른 검색 타입이 있습니다.

  • LabelText: getByLabelText: <label for="search" />
  • PlaceholderText: getByPlaceholderText: <input placeholder="Search" />
  • AltText: getByAltText: <img alt="profile" />
  • DisplayValue: getByDisplayValue: <input value="JavaScript" />

그리고 소스 코드의 HTML에 속성을 할당해야 하는 마지막 검색 타입인 TestId가 있습니다. 결국, 리액트 테스트 라이브러리와 렌더링 된 리액트 컴포넌트에서 엘리먼트를 선택하려면 go-to 검색 타입이어야 한다.

  • getByText
  • getByRole
  • getByLabelText
  • getByPlaceholderText
  • getByAltText
  • getByDisplayValue

다시 말하지만, 이들은 React Testing Library에서 사용할 수있는 모든 다양한 검색 타입이다.

리액트 테스트 라이브러리: 검색 VARIANTS

검색 타입과 달리 검색 변형도 있습니다. React Testing Library의 검색 변형 중 하나는 getByText 또는 getByRole에 사용되는 getBy입니다. React 컴포넌트를 테스트 할 때 기본적으로 사용되는 검색 변형이기도합니다.

다른 두 가지 검색 변형은 queryBy 및 findBy입니다. 둘 다 getBy가 액세스 할 수있는 동일한 검색 타입에 의해 확장 될 수 있습니다. 예를 들어 모든 검색 타입이 있는 queryBy는 다음과 같습니다.

  • queryByText
  • queryByRole
  • queryByLabelText
  • queryByPlaceholderText
  • queryByAltText
  • queryByDisplayValue

And findBy with all its search types:

  • findByText
  • findByRole
  • findByLabelText
  • findByPlaceholderText
  • findByAltText
  • findByDisplayValue

getBy와 queryBy의 차이점은 무엇입니까?

방의 큰 질문 : 언제 getBy를 사용하고 다른 두 가지 변형 인 queryBy와 findBy를 언제 사용할지. getBy가 엘리먼트 또는 오류를 반환한다는 것을 이미 알고 있습니다. 오류를 반환하는 getBy의 편리한 side-effect입니다. 개발자로서 테스트에 문제가 있음을 조기에인지 할 수 있기 때문입니다. 그러나 이로 인해 없어야하는 엘리먼트를 확인하기가 어렵습니다.

import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () => {
  test('renders App component', () => {
    render(<App />);

    screen.debug();

    // fails
    expect(screen.getByText(/Searches for JavaScript/)).toBeNull();
  });
});

디버그 출력에 "Searches for JavaScript"이라는 텍스트가있는 엘리먼트가없는 것으로 표시 되더라도이 텍스트가있는 엘리먼트를 찾을 수 없기 때문에 getBy가 assertion을 만들기 전에 오류를 던지기 때문입니다. 존재하지 않는 엘리먼트를 assertion하기 위해 getBy를 queryBy와 교환 할 수 있습니다.

import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () => {
  test('renders App component', () => {
    render(<App />);

    expect(screen.queryByText(/Searches for JavaScript/)).toBeNull();
  });
});

따라서 엘리먼트가 없다고 assertion 할 때마다 queryBy를 사용하십시오. 그렇지 않으면 기본값은 getBy입니다. 그렇다면 findBy는 어떻습니까?

언제 findBy를 사용해야 합니까?

findBy 검색 variant은 결국 거기에 있게 될 비동기 엘리먼트에 사용됩니다. 적절한 시나리오를 위해 다음 기능 (검색 입력 필드와 독립적)을 사용하여 React 컴포넌트를 확장 해 보겠습니다. 초기 렌더링 후 App 컴포넌트는 시뮬레이션 된 API에서 사용자를 가져옵니다. API는 사용자 객체로 즉시 해결되는 JavaScript promise를 반환하고 컴포넌트는 promise의 사용자를 컴포넌트 state에 저장합니다. 컴포넌트가 업데이트되고 다시 렌더링됩니다. 이후에 조건부 렌더링은 컴포넌트 업데이트 후 "다음으로 로그인"을 렌더링해야합니다.

function getUser() {
  return Promise.resolve({ id: '1', name: 'Robin' });
}

function App() {
  const [search, setSearch] = React.useState('');
  const [user, setUser] = React.useState(null);

  React.useEffect(() => {
    const loadUser = async () => {
      const user = await getUser();
      setUser(user);
    };

    loadUser();
  }, []);

  function handleChange(event) {
    setSearch(event.target.value);
  }

  return (
    <div>
      {user ? <p>Signed in as {user.name}</p> : null}

      <Search value={search} onChange={handleChange}>
        Search:
      </Search>

      <p>Searches for {search ? search : '...'}</p>
    </div>
  );
}

promise가 resolve 됨으로 인해 첫 번째 렌더링에서 두 번째 렌더링까지 컴포넌트를 테스트하려면 promise가 비동기 적으로 해결 될 때까지 기다려야하므로 비동기 테스트를 작성해야합니다. 즉, 컴포넌트를 가져온 후 한 번 업데이트 한 후 사용자가 렌더링 될 때까지 기다려야합니다.

import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () => {
  test('renders App component', async () => {
    render(<App />);

    expect(screen.queryByText(/Signed in as/)).toBeNull();

    expect(await screen.findByText(/Signed in as/)).toBeInTheDocument();
  });
});

초기 렌더링 후 getBy 검색 변형 대신 queryBy를 사용하여 "Signed in as" 텍스트가 없다고 assertion합니다. 그런 다음 새 엘리먼트가 발견되기를 기다립니다. 그러면 결국 promise가 해결되고 컴포넌트가 다시 렌더링 될 때 발견됩니다.

이것이 실제로 작동한다고 생각하지 않는다면 다음 두 디버그 함수를 포함하고 명령 줄에서 해당 출력을 확인하십시오.

import React from 'react';
import { render, screen } from '@testing-library/react';

import App from './App';

describe('App', () => {
  test('renders App component', async () => {
    render(<App />);

    expect(screen.queryByText(/Signed in as/)).toBeNull();

    screen.debug();

    expect(await screen.findByText(/Signed in as/)).toBeInTheDocument();

    screen.debug();
  });
});

아직 존재하지 않지만 결국 나타날 엘리먼트에 대해 findBy 또는 queryBy 보다는 findBy를 사용하십시오. 누락된 엘리먼트를 어설션하는 경우 queryBy를 사용합니다. 그렇지 않으면 기본 값은 getBy 입니다.

다중(multiple) 엘리먼트는 어떻습니까?

세 가지 검색 변형 인 getBy, queryBy 및 findBy에 대해 배웠습니다. 모두 검색 타입 (예 : 텍스트, role, PlaceholderText, DisplayValue)과 연관 될 수 있습니다. 이러한 모든 검색 함수가 하나의 엘리먼트 만 반환하는 경우 여러 엘리먼트가 있는지 확인하는 방법 (예 : React 컴포넌트의 목록). 모든 검색 변형은 모든 단어로 확장 할 수 있습니다.

  • getAllBy
  • queryAllBy
  • findAllBy

이들 모두는 엘리먼트 배열을 반환하고 검색 타입과 다시 연결될 수 있습니다.

Assertive Functions

Assertive 함수는 assertion의 오른쪽에서 발생합니다. 이전 테스트에서 두 개의 assertive 함수, 및. 둘 다 주로 React Testing Library에서 엘리먼트가 있는지 여부를 확인하는 데 사용됩니다.

일반적으로 이러한 모든 Assertive Functions은 Jest에서 유래되었습니다. 그러나 React Testing Library는 이 API들을 확장한다. 이러한 모든 Assertive Functions은 create-react-app을 사용할 때 이미 설정된 추가 패키지로 제공됩니다.

  • toBeDisabled
  • toBeEnabled
  • toBeEmpty
  • toBeEmptyDOMElement
  • toBeInTheDocument
  • toBeInvalid
  • toBeRequired
  • toBeValid
  • toBeVisible
  • toContainElement
  • toContainHTML
  • toHaveAttribute
  • toHaveClass
  • toHaveFocus
  • toHaveFormValues
  • toHaveStyle
  • toHaveTextContent
  • toHaveValue
  • toHaveDisplayValue
  • toBeChecked
  • toBePartiallyChecked
  • toHaveDescription

리액트 테스트 라이브러리: 이벤트 실행

지금까지 getBy (및 queryBy)를 사용하여 React 컴포넌트에서 엘리먼트가 렌더링되었는지 여부와 다시 렌더링 된 React 컴포넌트에 원하는 엘리먼트 (findBy)가 있는지 여부 만 테스트했습니다. 실제 사용자 인터랙션은 어떻습니까? 사용자가 입력 필드에 입력하면 컴포넌트가 다시 렌더링 될 수 있으며 (예제처럼) 새 값이 표시되어야 합니다 (또는 어딘가에서 사용).

React Testing Library의 fireEvent 함수를 사용하여 엔드 유저와의 인터랙션을 시뮬레이션 할 수 있습니다. 이것이 입력 필드에서 어떻게 작동하는지 봅시다 :

import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';

import App from './App';

describe('App', () => {
  test('renders App component', () => {
    render(<App />);

    screen.debug();

    fireEvent.change(screen.getByRole('textbox'), {
      target: { value: 'JavaScript' },
    });

    screen.debug();
  });
});

fireEvent 함수는 엘리먼트 (여기서는 텍스트 상자 role의 입력 필드)와 이벤트 (여기서는 "JavaScript"값을 가진 이벤트)를 받습니다. 디버그 함수의 출력은 이벤트 전후의 HTML 구조를 보여야 합니다. 입력 필드의 새 값이 적절하게 렌더링되는 것을 볼 수 있습니다.

또한 컴포넌트가 사용자를 가져 오기 때문에 App 컴포넌트와 같이 비동기 작업에 관여하는 경우 다음 경고가 표시 될 수 있습니다. "Warning: An update to App inside a test was not wrapped in act(...)." 우리에게 이것은 비동기 작업이 발생하고 컴포넌트가 이를 처리하는지 확인해야 함을 의미합니다. 종종 이것은 React Testing Library의 act 함수로 수행 할 수 있지만 이번에는 사용자가 해결할 때까지 기다려야 합니다.

describe('App', () => {
  test('renders App component', async () => {
    render(<App />);

    // wait for the user to resolve
    // needs only be used in our special case
    await screen.findByText(/Signed in as/);

    screen.debug();

    fireEvent.change(screen.getByRole('textbox'), {
      target: { value: 'JavaScript' },
    });

    screen.debug();
  });
});

그런 다음 이벤트 전후의 assertions을 할 수 있습니다.

describe('App', () => {
  test('renders App component', async () => {
    render(<App />);

    // wait for the user to resolve
    // needs only be used in our special case
    await screen.findByText(/Signed in as/);

    expect(screen.queryByText(/Searches for JavaScript/)).toBeNull();

    fireEvent.change(screen.getByRole('textbox'), {
      target: { value: 'JavaScript' },
    });

    expect(screen.getByText(/Searches for JavaScript/)).toBeInTheDocument();
  });
});

우리는 queryBy 검색 variant을 사용하여 이벤트 전에 엘리먼트가 없는지 확인하고 getBy 검색 variant을 사용하여 이벤트 이후에 엘리먼트가 있는지 확인했습니다. 때로는 사람들이 후자의 assertion에 대해 queryBy를 사용하는 것을 볼 수 있습니다. 왜냐하면 거기에 있어야 할 엘리먼트에 관해서는 getBy와 유사하게 사용할 수 있기 때문입니다.

그거다. 테스트에서 해결해야하는 비동기 동작 외에도 React Testing Library의 fireEvent 함수를 간단하게 사용할 수 있으며 나중에 어설션을 만들 수 있습니다.

리액트 테스트 라이브러리: 사용자 이벤트

React Testing Library는 fireEvent API 위에 구축되는 확장 된 사용자 이벤트 라이브러리와 함께 제공됩니다. 이전에는 사용자 상호 작용을 트리거하기 위해 fireEvent를 사용했습니다. 이번에는 userEvent API가 fireEvent API보다 실제 브라우저 동작을 더 가깝게 모방하기 때문에 userEvent를 대체로 사용할 것입니다.

For example, a triggers only a event whereas triggers a event, but also , , and events.

import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import App from './App';

describe('App', () => {
  test('renders App component', async () => {
    render(<App />);

    // wait for the user to resolve
    await screen.findByText(/Signed in as/);

    expect(screen.queryByText(/Searches for JavaScript/)).toBeNull();

    await userEvent.type(screen.getByRole('textbox'), 'JavaScript');

    expect(
      screen.getByText(/Searches for JavaScript/)
    ).toBeInTheDocument();
  });
});

가능하면 리액트 테스트 라이브러리를 사용할 때 fireEvent를 통해 사용자 이벤트를 사용합니다. 이 글을 쓰는 시점에서 userEvent는 fireEvent의 모든 기능을 포함하지 않지만 나중에 변경될 수 있습니다.

리액트 테스트 라이브러리: 콜백 핸들러

때로는 단위 테스트로 React 컴포넌트를 분리하여 테스트합니다. 종종 이러한 컴포넌트에는 사이드 이펙트나 state가 없지만 입력 (props) 및 출력 (JSX, 콜백 핸들러) 만 있습니다. 우리는 이미 컴포넌트와 props가 주어 졌을 때 렌더링 된 JSX를 어떻게 테스트 할 수 있는지 보았습니다. 이제이 검색 컴포넌트에 대한 콜백 핸들러를 테스트합니다.

function Search({ value, onChange, children }) {
  return (
    <div>
      <label htmlFor="search">{children}</label>
      <input
        id="search"
        type="text"
        value={value}
        onChange={onChange}
      />
    </div>
  );
}

모든 렌더링 및 어설션은 이전과 같이 발생합니다. 그러나 이번에는 Jest의 유틸리티를 사용하여 컴포넌트에 전달되는 함수를 모의합니다. 그런 다음 입력 필드에서 사용자 상호 작용을 트리거 한 후 콜백 함수가 호출되었음을 확인할 수 있습니다.

describe('Search', () => {
  test('calls the onChange callback handler', () => {
    const onChange = jest.fn();

    render(
      <Search value="" onChange={onChange}>
        Search:
      </Search>
    );

    fireEvent.change(screen.getByRole('textbox'), {
      target: { value: 'JavaScript' },
    });

    expect(onChange).toHaveBeenCalledTimes(1);
  });
});

여기서도 userEvent가 fireEvent와 같이 브라우저의 사용자 행동과 더 가깝게 일치하는지 확인할 수 있습니다. fireEvent는 콜백 함수를 한 번만 호출하여 change 이벤트를 실행하지만 userEvent는 모든 키 입력에 대해 이를 트리거합니다.

describe('Search', () => {
  test('calls the onChange callback handler', async () => {
    const onChange = jest.fn();

    render(
      <Search value="" onChange={onChange}>
        Search:
      </Search>
    );

    await userEvent.type(screen.getByRole('textbox'), 'JavaScript');

    expect(onChange).toHaveBeenCalledTimes(10);
  });
});

어쨌든, React Testing Library는 React 컴포넌트를 너무 격리하지 않고 다른 컴포넌트와의 통합 (통합 테스트)에서 테스트하도록 권장합니다. 그래야만 state 변경이 DOM에 적용되었는지 여부와 사이드 이펙트가 적용되었는지 여부를 실제로 테스트 할 수 있습니다.

리액트 테스트 라이브러리: ASYNCHRONOUS / ASYNC

특정 엘리먼트가 findBy 검색 variant과 함께 나타날 때까지 기다리기 위해 React Testing Library로 테스트 할 때 async await를 사용하는 방법을 이전에 보았습니다. 이제 React에서 데이터 가져 오기를 테스트하기 위한 간단한 예제를 살펴 보겠습니다. 원격 API에서 데이터를 가져 오기 위해 axios를 사용하는 다음 React 컴포넌트를 살펴 보겠습니다.

import React from 'react';
import axios from 'axios';

const URL = 'http://hn.algolia.com/api/v1/search';

function App() {
  const [stories, setStories] = React.useState([]);
  const [error, setError] = React.useState(null);

  async function handleFetch(event) {
    let result;

    try {
      result = await axios.get(`${URL}?query=React`);

      setStories(result.data.hits);
    } catch (error) {
      setError(error);
    }
  }

  return (
    <div>
      <button type="button" onClick={handleFetch}>
        Fetch Stories
      </button>

      {error && <span>Something went wrong ...</span>}

      <ul>
        {stories.map((story) => (
          <li key={story.objectID}>
            <a href={story.url}>{story.title}</a>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

버튼을 클릭하면 Hacker News API에서 기사 목록을 가져옵니다. 모든 것이 올바르게 진행되면 React에서 목록으로 렌더링 된 스토리 목록이 표시됩니다. 문제가 발생하면 오류가 표시됩니다. 앱 컴포넌트에 대한 테스트는 다음과 같습니다.

import React from 'react';
import axios from 'axios';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import App from './App';

jest.mock('axios');

describe('App', () => {
  test('fetches stories from an API and displays them', async () => {
    const stories = [
      { objectID: '1', title: 'Hello' },
      { objectID: '2', title: 'React' },
    ];

    axios.get.mockImplementationOnce(() =>
      Promise.resolve({ data: { hits: stories } })
    );

    render(<App />);

    await userEvent.click(screen.getByRole('button'));

    const items = await screen.findAllByRole('listitem');

    expect(items).toHaveLength(2);
  });
});

App 컴포넌트를 렌더링하기 전에 API가 mock 되었는지 확인합니다. 우리의 경우 메서드에서 axios의 반환 값이 mock 됩니다. 그러나 데이터 fetch를 위해 다른 라이브러리 또는 브라우저의 fetch API를 사용하는 경우 이러한 항목을 mock 처리해야합니다.

API를 mock하고 컴포넌트를 렌더링 한 후 userEvent API를 사용하여 API 요청으로 연결되는 버튼을 클릭합니다. 요청이 비동기식이므로 컴포넌트가 업데이트 될 때까지 기다려야합니다. 이전과 마찬가지로 React Testing Library의 findBy 검색 변형을 사용하여 결국 나타나는 엘리먼트를 기다립니다.

import React from 'react';
import axios from 'axios';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import App from './App';

jest.mock('axios');

describe('App', () => {
  test('fetches stories from an API and displays them', async () => {
    ...
  });

  test('fetches stories from an API and fails', async () => {
    axios.get.mockImplementationOnce(() =>
      Promise.reject(new Error())
    );

    render(<App />);

    await userEvent.click(screen.getByRole('button'));

    const message = await screen.findByText(/Something went wrong/);

    expect(message).toBeInTheDocument();
  });
});

이 마지막 테스트는 실패한 React 컴포넌트의 API 요청을 테스트하는 방법을 보여줍니다. 성공적으로 해결되는 promise로 API를 mock하는 대신 오류와 함께 promise를 거부합니다. 컴포넌트를 렌더링하고 버튼을 클릭 한 후 오류 메시지가 나타날 때까지 기다립니다.

import React from 'react';
import axios from 'axios';
import { render, screen, act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import App from './App';

jest.mock('axios');

describe('App', () => {
  test('fetches stories from an API and displays them', async () => {
    const stories = [
      { objectID: '1', title: 'Hello' },
      { objectID: '2', title: 'React' },
    ];

    const promise = Promise.resolve({ data: { hits: stories } });

    axios.get.mockImplementationOnce(() => promise);

    render(<App />);

    await userEvent.click(screen.getByRole('button'));

    await act(() => promise);

    expect(screen.getAllByRole('listitem')).toHaveLength(2);
  });

  test('fetches stories from an API and fails', async () => {
    ...
  });
});

완전성을 위해 이 마지막 테스트는 HTML이 표시 될 때까지 기다리지 않으려는 경우에도 작동하는것보다 명시적인 방식으로 promise를 기다리는 방법을 보여줍니다.

결국 React Testing Library를 사용하여 React에서 비동기 동작을 테스트하는 것은 그리 어렵지 않습니다. Jest를 사용하여 외부 모듈 (여기서는 원격 API)을 모의 한 다음 데이터를 기다리거나 테스트에서 React 컴포넌트를 다시 렌더링해야합니다.

React Testing Library는 React 컴포넌트에 대한 필자의 테스트 라이브러리입니다. 저는 이전에 Airbnb의 Enzyme을 계속 사용해 왔지만 React Testing Library가 구현 세부 사항이 아닌 사용자 행동을 테스트하는 방식을 좋아합니다. 실제 사용자 시나리오와 유사한 테스트를 작성하여 사용자가 애플리케이션을 사용할 수 있는지 테스트하고 있습니다.

Comments