[React] Jest, React Testing Library

ㅎㅇ·2024년 12월 17일

리액트심화스터디

목록 보기
2/2

오늘은 리액트에서의 test에 대해 알아보겠다.

테스트 코드란 무엇인지?

테스트 코드란, 소프트웨어의 기능과 동작을 테스트할 수 있는 코드이다. 개발자가 작성한 코드를 실행하고 예상한 결과가 나오는 지 확인하는 데 사용된다.
테스트 코드엔 단위 테스트(Unit Testing), 통합 테스트(Integration Testing), 시스템 테스트(System Testing) 등 다양한 종류가 있고, 종류에 따라 테스트의 대상과 범위가 다르다. 그러나 모두 기대한 입력값과 출력값을 반환하는지를 test한다.

다음은 여러가지 테스트 코드 종류이다.

테스트 코드를 작성했을 때의 장점

  • 코드 품질 및 안정성 향상 : 의도치 않은 버그를 사전에 찾아내고 수정할 수 있다.
  • 문서화 : 테스트 코드는 개발자가 기능의 동작 방식을 이해하는 데 도움이 되는 문서로 이용될 수 있다.
  • 리팩토링에 용이 : 테스트 코드로 리팩토링 후에도 기능이 정상 작동하는지 확인할 수 있기 때문에, 코드 수정이나 구조 변경에 대한 두려움을 줄일 수 있다.

테스트 코드를 작성했을 때의 단점

  • 개발 시간 증가 : 초기 개발 비용이 증가할 수 있으나, 장기적으로는 버그 수정 및 유지 보수 비용 절감으로 이어질 수 있다.
  • 불완전한 테스트 : 모든 버그를 잡아내는 테스트 코드를 작성하기 어렵고, 일부 버그가 통과하여 완전한 테스트가 어려운 경우가 있다.
  • 오버 엔지니어링 및 유지 보수 비용 증가
  • 높은 러닝 커브로 인한 개발 속도 저하

테스팅 도구를 선택할 때 고려할 수 있는 것

1️⃣ 작업의 속도 vs 실제 개발 환경과의 유사성

몇몇 테스팅 도구들은 변경사항이 생기고 결과 값이 출력되는 과정에서 매우 빠른 피드백 루프를 제공하지만 실제 브라우저 동작을 정확히 구현하지 않는다(ex. Jest).

어떠한 도구들은 현실 브라우저 환경과 동일하게 구현되지만 반복 작업 속도가 느리고 지속적 통합 서버 환경(CI)에 취약하다(ex. Cypress).

2️⃣ 얼마나 다양한 방법을 통해 실제 테스트 환경의 동작을 테스트할 것인가?

유닛 테스트: 함수 하나하나와 같이 코드의 작은 부분을 테스트하는 것
통합 테스트: 서로 다른 시스템들의 상호작용이 잘 이루어 지는지 테스트하는 것

컴포넌트 안에서는 유닛테스트와 통합테스트의 차이는 명확하지 않다.

👉 프로젝트에 따라 적합한 테스트를 도입해야 할 것


🎱 Jest

특징

  • 페이스북(현 메타)에서 만든 JavaScript 테스트 라이브러리
  • Test Runner와 Test Matcher 그리고 Test Mock까지 all-in-one으로 해결이 가능하다는 이점이 있음 (여러 라이브러리 조합할 필요 X, jest 하나로 해결 가능)
  • 빠른 피드백 루프 제공, 유닛 테스트에 최적화
  • 현실 브라우저 동작을 완벽히 재현하지는 못함

사용법

  1. 라이브러리 설치
yarn add jest --dev
  1. pakage.json 파일을 열고 test script를 jest로 수정
  "scripts": {
    "test": "jest"
  }
  1. 파일명.test.js 파일 생성 후에 테스트 코드 작성
describe('계산 테스트', () => {
   const a = 1, b = 2;

   test('a + b는 3이다.', () => {
      expect(a + b).toEqual(3);
   });
});
  • describe : 테스트 그룹을 묶어주는 역할. 블록 안의 콜백함수 내에 테스트에 쓰일 변수,객체들을 선언하여 일회용으로 사용

다음 코드를 작성 후 터미널에서 test를 실행하면 아래와 같이 뜬다. (예시)

테스트 실행 명령어를 입력하면 애플리케이션 내의 모든 테스트 파일이 실행된다. (특정 테스트 파일만 실행시키고 싶다면 명령어 뒤에 경로 입력해주기)

test("테스트 설명", () => {
  expect("검증 대상").toXxx("기대 결과");
});

일반적인 패턴은 보통 위의 예시와 같다.
toXxx 부분에서 사용되는 함수를 Test Matcher라고 한다.

자주 사용되는 Test Matcher

  • toEqual : 값을 비교할 때 사용 (위에 예시 참고)
  • toBeTruthy(), toBeFalsy()
    test("number 0 is falsy but string 0 is truthy", () => {
      expect(0).toBeFalsy();
      expect("0").toBeTruthy();
    });
  • toHaveLength(), toContain() : 배열이 길이를 체크하거나 특정 원소가 존재 여부를 테스트하는 경우
    test("array", () => {
      const colors = ["Red", "Yellow", "Blue"];
      expect(colors).toHaveLength(3); //배열의 길이 체크
      expect(colors).toContain("Yellow"); //존재 여부 체크
      expect(colors).not.toContain("Green");
    });
  • toMatch() : 문자열이 일치하는지 정규식 기반의 테스트가 필요할 때
    test("string", () => {
      expect(getUser(1).email).toBe("user1@test.com"); //문장 통째로 일치하는지
      expect(getUser(2).email).toMatch(/.*test.com$/); //정규식 기반
    });
  • toThrow() : 예외 발생 여부를 테스트할 때
    문자열을 인자로 넘기면 예외 메시지를 비교, 정규식을 인자로 넘기면 정규식 체크
    반드시 expect() 함수로 한 번 감싸줘야 함
    function getUser(id) {
      if (id <= 0) throw new Error("Invalid ID");
      return {
        id,
        email: `user${id}@test.com`,
      };
    }

	test("throw when id is non negative", () => {
      expect(() => getUser(-1)).toThrow();
      expect(() => getUser(-1)).toThrow("Invalid ID");
    });

🎱 React Testing Library

먼저 라이브러리를 소개하기 전에, 행위 주도 테스트와 구현 주도 테스트에 대해 알아보고 들어가겠다.

구현 주도 테스트 (Implementation Driven Test)

<h2 class="title">제목</h2>
  • 애플리케이션이 어떻게 작동하는지에 대해 초점을 맞추어 테스트 작성
  • 위의 예시 UI에 대해 테스트를 작성한다고 하면 class 이름이 title인지와 h2 태그의 사용 여부에 집중한다.

👉 즉, h2태그를 span태그로 수정할 시 테스트 에러 발생. 화면에 그려지는 UI는 똑같을텐데 이 에러가 유의미한지 의문이다.

행위 주도 테스트 (Behavior Driven Test)

반면 행위 주도 테스트는 위의 구현 주도 테스트의 단점을 보완하여, 화면이 어떻게 변화하는지에 초점을 맞추어 테스트를 작성한다. (즉, 사용자의 실제 경험 위주로 테스트를 작성)

Enzyme vs React Testing Library

리액트 테스팅 라이브러리가 등장하기 전, Airbnb에서 만든 Enzyme이라는 테스팅 라이브러리가 많이 사용되었다.

Enzyme

  • 구현 주도 테스트에 적합
  • 실제 브라우저 DOM이 아닌 리액트의 가상 DOM을 기준으로 테스트를 작성하기 때문
  • 테스트 대상 컴포넌트의 prop, state에 대해 검증을 수행

React Testing Library

  • 행위 주도 테스트에 적합
  • jsdom이나 happy-dom 라이브러리를 통해 실제 브라우저 DOM을 기준으로 테스트 작성
  • 사용자 브라우저에서 렌더링하는 실제 html 마크업의 모습이 어떤지에 대해서 테스트하기 용이

RTL 사용법

RTL 설치 이전에, 리액트 프로젝트에 테스팅 프레임워크(jest, vitest 등)가 설치돼있어야 한다.
cf) create-react-app으로 생성된 프로젝트는 jest와 테스팅 라이브러리가 이미 설치되어있음

RTL 설치

yarn add -D @testing-library/react

jest-dom을 설치 (jest-dom에 특화된 custom matcher 제공)

yarn add -D @testing-library/jest-dom

RTL 사용할 때 필수로 해줘야 하는 설정

  1. 각 테스트가 DOM에 렌더링해놓은 내용들을 테스트가 끝날 때 지워주는 설정
  2. jest-dom가 제공하는 matcher를 Jest 테스트 러너에게 인식시키는 설정
// create-react-app으로 생성된 프로젝트일 경우
import "@testing-library/react/cleanup-after-each";
import "@testing-library/jest-dom/extend-expect";
// vitest를 사용하는 프로젝트일 경우
import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { afterEach } from "vitest";

afterEach(cleanup);

주요 API

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

render(<YourComponent />);

const button = screen.getByText(/click me/i);
fireEvent.click(button);
  • render() : 인자로 넘어온 JSX를 DOM에 렌더링
  • screen : DOM에서 특정 영역을 선택하기 위한 다양한 쿼리함수를 제공하는 객체
  • fireEvent : 특정 이벤트를 발생시켜주는 객체, 유저와 상호작용 테스트할 경우 필요
  • 다양한 쿼리 함수 참고 🔽
    https://testing-library.com/docs/dom-testing-library/cheatsheet

사용 예시

아래 코드는 이메일과 비밀번호 입력란, 버튼으로 구성된 간단한 로그인폼 컴포넌트이다. 내부 상태에 따라 UI 변화가 생길 수 있는 컴포넌트에 해당한다.

import React from "react";

function LoginForm({ onSubmit }) {
  const [email, setEmail] = React.useState("");
  const [password, setPassword] = React.useState("");

  return (
    <>
      <h2>Login</h2>
      <form onSubmit={() => onSubmit()}>
        <label>
          이메일
          <input
            type="email"
            placeholder="user@test.com"
            value={email}
            onChange={({ target: { value } }) => setEmail(value)}
          />
        </label>
        <label>
          비밀번호
          <input
            type="password"
            value={password}
            onChange={({ target: { value } }) => setPassword(value)}
          />
        </label>
        <button disabled={!email || !password}>로그인</button>
      </form>
    </>
  );
}

아래는 비활성화 되어 있던 로그인 버튼이 이메일, 비밀번호 입력 후에 활성화되는지에 대한 테스트 코드이다.

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

describe("<LoginForm />", () => {
  it("enables button when both email and password are entered", () => {
    render(<LoginForm onSubmit={() => null} />);

    const button = screen.getByRole("button", {
      name: /로그인/i,
    });
    expect(button).toBeDisabled();

    const email = screen.getByRole("textbox", {
      name: /이메일/i,
    });
    const password = screen.getByLabelText(/비밀번호/i);

    fireEvent.change(email, { target: { value: "user@test.com" } });
    fireEvent.change(password, { target: { value: "Test1234" } });

    expect(button).toBeEnabled();
  });
});
  • getByRole(), getByLabelText라는 쿼리 함수로 로그인 버튼과 입력란을 선택
    (type 속성이 password로 설정된 input 요소에는 아무 역할이 주어지지 않음)
  • jest-dom의 toBeDisAbled()와 toBeEnabled() matcher 함수를 통해 로그인 버튼의 활성화 여부를 이벤트 발생 전후로 검증
  • 로그인 버튼을 클릭했을 때, prop으로 넘긴 onSubmit 함수가 호출되는지 여부를 검증
profile
안녕하세요

1개의 댓글

comment-user-thumbnail
2024년 12월 18일

우와 테스트 코드에 대한 이해부터 Jest랑 RTL의 사용 예시까지 하나하나 다뤄주셨네요!! 덕분에 흐름에 따라 이해하기 쉬웠던 것 같아요! TDD를 도입해보고 싶었는데 작성해주신 고려할 부분들을 생각해보고 알맞게 도입해야겠습니다.

답글 달기