본문 바로가기

React/실험실

[React] 벨로퍼트와 함께하는 React Testing - react-testing-library

들어가며

벨로퍼트님의 테스팅 튜토리얼을 공부한 내용을 정리하는 글입니다. 

 

이 전에 Test Coverage를 공부할 때 한번 정리했지만 사용의 전반적인 흐름이 더 좋다고 느껴서

다시 정리한다. 

 

react-testing-library

모든 테스트를 DOM 위주로 진행하는 방식이다. 

컴포넌트를 리팩토링할 때, 내부 구조 및 네이밍은 많이 바뀔 수 있지만 작동 방법은 크게 바뀌지 않는다.

 

react-testing-library는 이러한 특징을 중요하게 생각해서 컴포넌트의 기능이 똑같이 작동한다면, 

컴포넌트의 내부 구현 방식이 변해도 테스트가 실패하지 않도록 설계되어있다. 

 

테스트 코드 

const Profile = ({ username, name }) => {
  return (
    <div>
      <b>{username}</b>
      <span>({name})</span>
    </div>
  );
};

export default Profile;

간단한 컴포넌트를 하나 만들었다. 

이런 컴포넌트는 어떤 방식으로 테스트 코드를 작성하면 될까? 

 

import { render, screen } from "@testing-library/react";
import Profile from ".";

describe("<Profile />", () => {

  const user = {
    username: "ing-yeo",
    name: "서재완",
  };

  test("matches snapshot", () => {
    const utils = render(<Profile {...user} />);

    expect(utils.container).toMatchSnapshot();
  });

  test("shows the props correctly", () => {
    render(<Profile username="ing-yeo" name="서재완" />);

    screen.getByText("ing-yeo");
    screen.getByText("(서재완)");
  });
});

react-testing-library에서 컴포넌트를 렌더링 할 때는 render 함수를 사용한다. 

함수가 호출되면 다양한 테스팅이 가능한데, 하나씩 알아보자 

 

스냅샷 테스팅 

스냅샷 테스팅은 렌더링된 결과가 이전 렌더링한 결과와 일치하는지 확인하는 작업을 의미한다. 

const user = {
  username: "ing-yeo",
  name: "서재완",
};

  test("matches snapshot", () => {
    const utils = render(<Profile {...user} />);

    expect(utils.container).toMatchSnapshot();
  });

container는 해당 컴포넌트의 최상위 컴포넌트를 나타내고, 최초에 snapshot을 사용하게 되면 

_snapshots_라는 폴더에 컴포넌트의 정보를 저장한다. 

 

그리고 다음에 다시 toMatchSnapshot을 사용하면 _snapshots_폴더의 정보와 비교해서 일치하는지 

확인한다. 

 

만약 다를 경우 

아래와 같이 snapshot과 다르다고 표시된다. 

렌더링된 결과가 이전 렌더링한 결과와 일치하는지 확인해야 한다면 snapshot을 사용하면 된다. 

 

다양한 쿼리

render 함수를 실행하면 screen에서 많은 쿼리를 사용할 수 있다. 

이러한 쿼리는 Variant와 Queries의 조합으로 네이밍이 이루어져있다. 

 

Variant 

getBy

getBy 로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 하나를 선택한다. 

만약 없다면 에러가 발생한다.

 

getAllBy 

getAllBy 로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 여러개를 선택한다. 

만약 하나도 없다면 에러가 발생한다. 

 

queryBy 

queryBy 로 시작하는 쿼리는 조건이 일치하는 DOM 엘리먼트 하나를 선택한다. 

존재하지 않아도 에러가 발생하지 않는다. 

 

queryAllBy 

queryAllBy 로 시작하는 쿼리는 조건이 일치하는 DOM 엘리먼트 여러개를 선택한다. 

만약 하나도 존재하지 않더라도 에러가 발생하지 않는다. 

 

findBy 

findBy 로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 하나가 나타날 때 까지 기다렸다가 

해당 DOM을 선택하는 Promise를 반환한다. 

기본 timeout인 4500ms 이후에도 나타나지 않는다면 에러가 발생한다. 

 

findAllBy

findAllBy 로 시작하는 쿼리는 조건에 일치하는 DOM 엘리먼트 여러개가 나타날 때 까지 기다렸다가 

해당 DOM을 선택하는 Promise를 반환한다. 

기본 timeout인 4500ms 이후에도 나타나지 않는다면 에러가 발생한다. 

 

Queries

ByLabelText 

label이 있는 input의 label 내용으로 input을 선택한다. 

 

ByPlaceholderText

placeholder 값으로 input 및 textarea를 선택한다. 

 

ByText

엘리먼트가 가지고 있는 텍스트 값으로 DOM을 선택한다. 

 

ByAltText 

alt 속성을 가지고 있는 엘리먼트를 선택한다. 

 

ByTitle

title 속성을 가지고 있는 DOM 또는 title 엘리먼트를 지니고 있는 svg를 선택할 때 사용한다. 

 

ByDisplayValue

input, textarea, select가 지니고 있는 현재 값을 가지고 엘리먼트를 선택한다.

 

ByRole 

특정 role 값을 지니고 있는 엘리먼트를 선택한다. 

 

ByTestId 

다른 방법으로 선택하지 못할 경우 사용하는 방법으로 DOM에 직접 test할 id를 달아서 선택한다. 

 

어떤 쿼리를 사용해야 할까? 

Variant + Query 조합을 하면 엉청 많은 쿼리가 생기는데 어떤 쿼리를 우선적으로 사용해야 할까? 

react-testing-library는 아래와 같은 우선순위를 따라 사용하는 것을 권장한다. 

 

1. getByLabelText

2. getByPlaceholderText

3. getByText

4. getByDisplayValue

5. getByAltText

6. getByTitle

7. getByRole

8. getByTestId 

 

Counter 컴포넌트 테스트 코드 작성하기 

import { useState } from "react";

const Counter = () => {
  const [number, setNumber] = useState(0);

  const handleIncrease = () => {
    setNumber((prev) => prev + 1);
  };

  const handleDecrease = () => {
    setNumber((prev) => prev - 1);
  };

  return (
    <div>
      <h2>{number}</h2>
      <button onClick={handleIncrease}>Up</button>
      <button onClick={handleDecrease}>Down</button>
    </div>
  );
};

export default Counter;

다음과 같이 아주 간단한 Counter 컴포넌트가 있다. 

이것을 테스트 코드를 작성하려면 어떤 방식으로 작성해야 할까? 

 

import { fireEvent, render, screen } from "@testing-library/react";
import Counter from ".";

describe("Counter Component", () => {
  test("matches snapshot", () => {
    const { container } = render(<Counter />);
    expect(container).toMatchSnapshot();
  });

  test("has a Number and Two Buttons", () => {
    render(<Counter />);

    screen.getByText("0");
    screen.getByText("Up");
    screen.getByText("Down");
  });

  test("increase", () => {
    render(<Counter />);

    const number = screen.getByText("0");
    const increaseButton = screen.getByText("Up");

    fireEvent.click(increaseButton);
    fireEvent.click(increaseButton);

    expect(number).toHaveTextContent("2");
    expect(number.textContent).toBe("2");
  });

  test("decrease", () => {
    render(<Counter />);

    const number = screen.getByText("0");
    const decreaseButton = screen.getByText("Down");

    fireEvent.click(decreaseButton);
    fireEvent.click(decreaseButton);

    expect(number).toHaveTextContent("-2");
    expect(number.textContent).toBe("-2");
  });
});

다음과 같이 테스트 코드를 작성할 수 있다. 

하나하나씩 살펴보자 

 

  test("matches snapshot", () => {
    const { container } = render(<Counter />);
    expect(container).toMatchSnapshot();
  });

Counter 컴포넌트는 props 등으로 디자인이 변경되지 않기 때문에 snapshot을 사용해서 

혹시라도 디자인이 변경되었는지 확인한다. 

 

  test("has a Number and Two Buttons", () => {
    render(<Counter />);

    screen.getByText("0");
    screen.getByText("Up");
    screen.getByText("Down");
  });

Counter 컴포넌트에서 숫자와 Up, Down 버튼이 있는지 확인한다. 

 

  test("increase", () => {
    render(<Counter />);

    const number = screen.getByText("0");
    const increaseButton = screen.getByText("Up");

    fireEvent.click(increaseButton);
    fireEvent.click(increaseButton);

    expect(number).toHaveTextContent("2");
    expect(number.textContent).toBe("2");
  });

  test("decrease", () => {
    render(<Counter />);

    const number = screen.getByText("0");
    const decreaseButton = screen.getByText("Down");

    fireEvent.click(decreaseButton);
    fireEvent.click(decreaseButton);

    expect(number).toHaveTextContent("-2");
    expect(number.textContent).toBe("-2");
  });

increase와 descrease가 제대로 작동하는지 확인한다. 

이때 이벤트를 발생시키고 싶다면 fireEvent를 사용해서 이벤트를 발생시킬 수 있다. 

반응형