리액트 스냅샷 테스트 with Typescript

2021. 4. 13. 18:10웹 프론트엔드 개발 노하우/리액트 노하우

이 글은 JEST 공식 문서의 내용을 바탕으로 작성되었습니다.

(참조: https://jestjs.io/docs/tutorial-react)

또한 아래의 영상의 내용을 참고하였습니다.

(참조: https://www.youtube.com/watch?v=g4rMWtPNOr8)

스냅샷 테스트?

 

단순히 사용자가 무언가를 클릭하면 어떤 요소가 어떻게 보여야 한다는 식의 E2E 테스트만 하는 것 보다는 이전 렌더링 결과물과 신규 렌더링 결과물(HTML)이 조금이라도 차이가 있는지를 파악하는 것이 더 명확하고 안전한 테스트라고 볼 수 있습니다.

 

그리고 그것을 가능하게 해주는 것이 스냅샷 테스트입니다. 스냅샷 테스트를 하면 기존에 저장되어 있는 이전 렌더링 결과와 테스트 시점에 새로 생성되는 렌더링 결과를 비교해서 조금이라도 차이가 있는지를 검사할 수 있습니다. 하지만 이를 리액트와 함께 어떻게 수행할 수 있는지, 주의할 점은 무엇인지에 대해서도 좀 더 다뤄보도록 하겠습니다.

 

리액트에서 스냅샷 테스트 수행하기

 

공식 문서에서 나온 예제를 바탕으로 설명하겠습니다.

(예제는 바닐라 자바스크립트로 되어 있어서 제가 임의로 타입스크립트로 조금 변환했습니다).

 

다음과 같은 Link 컴포넌트가 있을 때

 

//Link.tsx

import React from "react";

type LinkProps = {
  page: string;
};

type LinkState = {
  class: string;
};

const STATUS = {
  HOVERED: "hovered",
  NORMAL: "normal",
};

export default class Link extends React.Component<LinkProps, LinkState> {
  state = {
    class: STATUS.NORMAL,
  };

  _onMouseEnter = () => {
    this.setState({ class: STATUS.HOVERED });
  };

  _onMouseLeave = () => {
    this.setState({ class: STATUS.NORMAL });
  };

  render() {
    return (
      <a
        className={this.state.class}
        href={this.props.page || "#"}
        onMouseEnter={this._onMouseEnter.bind(this)}
        onMouseLeave={this._onMouseLeave.bind(this)}
      >
        {this.props.children}
      </a>
    );
  }
}

 

이 Link 컴포넌트에 마우스가 올려졌을 때, 해당 컴포넌트의 클래스 이름이 'normal' 에서 'hovered'로 바뀌고, 마우스를 다시 밖으로 옮겼을 때 클래스 이름이 'hovered' 에서 'normal' 로 원상복귀 되는지를 테스트하고 싶다고 가정해봅시다. 그렇다면 다음과 같은 테스트 코드를 작성할 수 있습니다

 

(마찬가지로 공식 사이트의 예제를 참고하였으며, 타입스크립트가 적용되어 있습니다. 예제 코드에서는 react-test-renderer 의 renderer 객체를 쓰지만, 그냥 RTL 의 render 메서드를 사용해도 무방합니다).

 

import React from "react";
import renderer, { ReactTestRendererJSON } from "react-test-renderer";
import Link from "./Link";

test("Link changes the class when hovered", () => {
  const component = renderer.create(<Link page="http://www.facebook.com">Facebook</Link>);
  const tree1 = component.toJSON() as ReactTestRendererJSON;
  expect(tree1).toMatchSnapshot();

  // manually trigger the callback
  tree1.props.onMouseEnter();
  // re-rendering
  const tree2 = component.toJSON() as ReactTestRendererJSON;
  expect(tree2).toMatchSnapshot();

  // manually trigger the callback
  tree2.props.onMouseLeave();
  // re-rendering
  const tree3 = component.toJSON() as ReactTestRendererJSON;
  expect(tree3).toMatchSnapshot();
});

 

위의 테스트 코드는 통과합니다. 그러나 이 테스트 코드를 보았을 때 드는 의문점이 크게 2가지 정도 들것입니다.

 

 

  1. snapshot 은 정확히 언제 생성되는 것이지?
  2. 왜 DOM 을 변경('normal' 클래스를 'hovered'로 변환함)한 상태에서 스냅샷 매칭을 다시 수행해도 통과가 되는거지?

 

 

Snapshot 의 생성

 

스냅샷은 'toMatchSnapshot' 메서드를 수행하는 것만으로도 해당 test 파일이 위치한 곳에 .snap 확장자명과 함께 생성됩니다. 'toMatchSnapshot' 메서드는 기존에 스냅샷 파일이 있는지를 확인하고, 없다면 지금의 렌더링 결과물을 바탕으로 스냅샷 파일을 생성하고, 스냅샷 파일이 있다면 해당 스냅샷과 지금 렌더링 결과물을 비교하는 일을 수행하는 것입니다.

 

 

 

toMatchSnapshot 의 복수실행

 

많은 예제에서는 toMatchSnapshot 메서드를 한번만 수행하는 경우가 많아서 알아채기 힘들지만, 사실 스냅샷은 해당 메서드가 수행되는 테스트 항목마다 여러개를 가질 수 있습니다. 따라서 toMatchSnapshot 메서드가 여러번 수행되면 생성되어 있는 스냅샷마다 순서대로 렌더링 결과물 비교를 수행하는 것입니다.

 

따라서 테스트 코드에서 toMatchSnapshot 메서드의 수행횟수를 줄이는 것만으로도, 스냅샷 테스트는 실패할 수 있습니다. 하나의 테스트에서 스냅샷을 여러개로 나눠서 각각을 비교하고 싶다면, 순서와 횟수를 일정하게 유지하는 것이 중요할 것입니다.