Logo

유저 이벤트 테스트 (@testing-library/user-event)

웹 애플리케이션을 개발할 때 브라우저 상에서 유저가 발생시키는 이벤트에 애플리케이션이 예상대로 반응하는지 어떻게 테스트할 수 있을까요? 사람이 직접 브라우저에서 해당 애플리케이션을 열고 어떤 내용을 입력하거나 특정 버튼을 클릭하면서 수동 테스트를 해야한다면 매우 비효율적일 것입니다. 이번 포스팅에서는 유저와 애플리케이션의 상호 작용을 검증하기 위한 테스트 코드를 작성하는 방법에 대해서 알아보겠습니다.

예제 컴포넌트

우선 테스트 대상이 될 간단한 React 컴포넌트 하나를 작성해보도록 하겠습니다.

아래 <LoginForm/> 컴포넌트는 이메일 입력란과 비밀번호 입력란, 로그인 버튼으로 구성되어 있습니다. 로그인 버튼은 이메일과 비밀번호가 입력되었을 때만 활성화가 되고, 클릭을 하면 prop으로 넘어온 onSubmit() 함수를 호출합니다.

import React, { useState } from "react";

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

  const handleSubmit = (event) => {
    event.preventDefault();
    onSubmit();
  };

  return (
    <form onSubmit={handleSubmit}>
      <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>
  );
}

테스트 환경 셋업

본 포스팅에서는 Testing Library를 사용하여 위에서 작성한 React 컴포넌트에 대한 테스트를 작성해보겠습니다. 본인 프로젝트에 관련 Testing Library 패키지가 설치되어 있지 않다면 다음과 같이 설치해줍니다.

$ npm i @testing-library/react @testing-library/jest-dom

React Testing Library에 대한 자세한 설명은 관련 포스팅를 참조바랍니다.

fireEvent로 유저 이벤트 발생시키기

먼저 이메일과 패스워드가 입력되어 있을 때만 로그인 버튼이 활성화되는지를 확인하기 위한 테스트 코드를 작성해보겠습니다. React Testing Library에서 제공하는 fireEvent를 사용하면 쉽게 유저 이벤트를 발생시킬 수 있습니다.

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

test("enables button when both email and password are entered", () => {
  render(<LoginForm />);

  const email = screen.getByLabelText("이메일");
  const password = screen.getByLabelText("비밀번호");
  const button = screen.getByRole("button");

  expect(button).toBeDisabled(); // 버튼 비활성화

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

  expect(button).toBeEnabled(); // 버튼 활성화
});

처음에는 <button> 엘리먼트가 비활성화 상태였는데 <input> 엘리먼트에 change 이벤트를 발생시키면 <button> 엘리먼트의 상태가 활성화되는 것을 검증하고 있습니다.

User Event 라이브러리

엄밀히 얘기해서 사용자가 <input> 엘리먼트에 데이터를 입력할 때는, change 이벤트 뿐만 아니라 focus, keydown, keyup과 같은 다양한 이벤트가 발생합니다. 따라서 React Testing Library에 내장되어 있는 fireEvent를 사용하면, 실제로 발생해야하는 모든 유저 이벤트를 발생되지 않는다는 단점이 있습니다.

Testing Library 에코 시스템의 일부인 User Event 라이브러리를 사용하면 마치 사람이 직접 브라우저 상에서 행동하는 것처럼 연관된 유저 이벤트를 한 번에 발생시킬 수 있습니다. User Event 라이브러리를 사용하려면 @testing-library/user-event npm 패키지를 설치해야 합니다.

$ npm i -D @testing-library/user-event

User Event 라이브러리로 유저 이벤트 발생시키기

위에서 fireEvent를 이용하여 작성한 테스트를 이번에는 User Event 라이브러리를 이용하여 재작성해보겠습니다.

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

test("enables button when both email and password are entered", () => {
  render(<LoginForm />);

  const email = screen.getByLabelText("이메일");
  const password = screen.getByLabelText("비밀번호");
  const button = screen.getByRole("button");

  expect(button).toBeDisabled(); // 버튼 비활성화

  async userEvent.type(email, "user@test.com");
  async userEvent.type(password, "Test1234");

  expect(button).toBeEnabled(); // 버튼 활성화
});

User Event 라이브러리는 fireEvent와 달리 사람의 행동에 가까운 좀 더 추상화된 함수명을 제공합니다. userEvent.type() 함수에는 target.value의 중첩 구조의 이벤트 객체를 넘길 필요가 없이, 실제 입력 텍스트만 넘기면 됩니다. 또한, change 이벤트 뿐만 아니라 focus, keydown, keyup과 같은 실제로 동반되야하는 모든 이벤트가 함께 발생하게 됩니다.

v14 업데이트: @testing-library/user-event 버전 14부터 모든 API가 비동기 함수로 바뀌었습니다. 따라서 반드시 앞에 async 키워드를 붙여주셔서 제대로 동작할 것입니다.

추가 테스트 예제 1

추가로 로그인 버튼을 비활성화된 상태에서 클릭하면 onSubmit() 함수가 호출되지 않는지를 검증하는 테스트 코드를 작성해보겠습니다.

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

test("can't submit form when button is disabled", () => {
  const obSubmit = jest.fn();
  render(<LoginForm onSubmit={obSubmit} />);

  const button = screen.getByRole("button");

  // fireEvent.click(button);
  async userEvent.click(button);

  expect(obSubmit).toHaveBeenCalledTimes(0);
});

추가 테스트 예제 2

반대로 로그인 버튼을 활성화된 상태에서 클릭하면 onSubmit() 함수가 호출되는지를 검증하는 테스트 코드를 작성해보겠습니다.

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

test("submits form when button is clicked", () => {
  const obSubmit = jest.fn();
  render(<LoginForm onSubmit={obSubmit} />);

  const email = screen.getByLabelText("이메일");
  const password = screen.getByLabelText("비밀번호");
  const button = screen.getByRole("button");

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

  async userEvent.type(email, "user@test.com");
  async userEvent.type(password, "Test1234");
  async userEvent.click(button);

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

전체 코드

본 포스팅에서 작성한 예제 코드와 테스트 코드는 아래에서 직접 확인하고 실행해보실 수 있습니다.

업데이트: Vitest로 작성한 테스트 코드는 아래에서 직접 확인하고 실행해보실 수 있습니다.

마치면서

이상으로 React 컴포넌트를 테스트할 때 유저 이벤트 발생시키는 2가지 방법에 대해서 알아보았습니다. React Testing Library에 내장되어 있는 fireEvent를 사용할 수도 있지만, User Event 라이브러리를 사용하면 좀 더 실제와 가까운 테스트를 작성할 수 있습니다.