Logo

React Testing Library 유저 이벤트 처리

브라우저 상에서 사용자가 발생시키는 이벤트에 웹 애플리케이션이 예상대로 반응하는지 어떻게 테스트할 수 있을까요?

이번 포스팅에서는 사용자와 애플리케이션의 상호 작용을 검증하기 위한 테스트 코드를 작성하는 방법에 대해서 알아보겠습니다.

예제 컴포넌트

우선 테스트 대상이 될 간단한 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로 유저 이벤트 발생시키기

먼저 이메일과 패스워드가 입력되어 있을 때만 로그인 버튼이 활성화되는지를 확인하기 위한 테스트 코드를 작성해보겠습니다. 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과 같은 다양한 이벤트가 발생합니다. 하지만 Testing Library에 내장되어 있는 fireEvent를 사용하면 실제로 발생해야 하는 모든 유저 이벤트가 발생되지 않는다는 문제점이 있습니다.

뿐만 아니라 사용자는 어떤 이벤트가 발생하고 그 이벤트 객체의 구조가 어떻게 생겼는지 관심이 없습니다. 단지 키보드로 텍스트를 입력할 뿐이죠. 따라서 사용자 입장에서 테스트를 작성해야한다는 Testing Library의 기본 원칙과도 부합하지 않습니다.

이러한 문제점을 해결을 하기 위해서 Testing Library는 유저 이벤트를 발생하기 위한 전용 패키지인 @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", async () => {
  const user = userEvent.setup();

  render(<LoginForm />);

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

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

  await user.type(email, "user@test.com");
  await user.type(password, "TEST1234");

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

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

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

추가 테스트 예제 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", async () => {
  const user = userEvent.setup();

  const obSubmit = jest.fn();
  render(<LoginForm onSubmit={obSubmit} />);

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

  // fireEvent.click(button);
  await user.click(button);

  expect(obSubmit).not.toHaveBeenCalled();
});

추가 테스트 예제 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", async () => {
  const user = userEvent.setup();

  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);

  await user.type(email, "user@test.com");
  await user.type(password, "TEST1234");
  await user.click(button);

  expect(obSubmit).toHaveBeenCalledOnce();
});

키보드 이벤트

마지막으로 키보드 사용자들도 애플리케이션과 원할히 상호작용할 수 있는지 테스트해볼까요? 탭으로 각 입력란으로 포커스를 이동하여 텍스트를 입력하고 엔터키를 눌러서 로그인 폼을 제출할 수 있어야 합니다.

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 using keyboard", async () => {
  const user = userEvent.setup();

  const obSubmit = jest.fn();
  render(<LoginForm onSubmit={obSubmit} />);

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

  await user.tab();
  await user.keyboard("user@test.com");
  await user.tab();
  await user.keyboard("TEST1234");
  await user.keyboard("{Enter}");

  expect(obSubmit).toHaveBeenCalledOnce();
});

전체 코드

본 포스팅에서 Jest 기준으로 작성한 코드는 아래에서 직접 확인하고 실행해보실 수 있습니다.

Vitest와 타입스크립트로 작성한 코드도 아래 올려두었으니 참고 바랍니다.

마치면서

지금까지 Testing Library로 React 컴포넌트를 테스트할 때 유저 이벤트를 발생시키는 두 가지 방법에 대해서 알아보았습니다. Testing Library에 내장되어 있는 fireEvent를 사용할 수도 있지만, User Event 패키지를 활용하면 좀 더 사용자가 실제로 애플리케이션과 상호 작용하는 것처럼 테스트를 작성할 수 있습니다.