[react-testing-library] fireEvent vs userEvent

pds·2023년 6월 4일
1

TIL

목록 보기
59/60

userEvent, fireEvent 차이를 알아보자

fireEvent

React Testing Library에서 제공하는 유틸리티 함수 중 하나로 테스트 중에 이벤트를 발생시키는 데 사용한다.

React 컴포넌트를 테스트할 때 사용자 동작을 모의하고 특정 이벤트를 발생시켜 컴포넌트의 상태 변화 및 렌더링을 확인하는 데 유용하다.

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

test('MyComponent renders correctly', () => {
  render(<MyComponent />);
  
  // 버튼 클릭 이벤트 발생
  const button = screen.getByRole('button');
  fireEvent.click(button);
  
  // 상태 변화 확인
  const message = screen.getByText('Button clicked');
  expect(message).toBeInTheDocument();
});

react-testing-library에서 단순히 처음에 보여지는 부분들에 대해서 테스트할 뿐 아니라 컴포넌트에서 사용자 상호작용 및 이벤트가 발생했을 때에 어떤 변화가 있는지 검증하기 위해 fireEvent가 필요한 것이다.

click(), mouseover(), focus(), change()등 이벤트를 트리거할 수 있는 다양한 메소드들을 제공하고 테스트 코드에서 적절한 엘리먼트에 이벤트를 fire 하면 이벤트가 발생하고 실제 컴포넌트에서 작성된 이벤트에 대한 변화를 확인할 수 있다.


userEvent

의존성

yarn add -D @testing-library/user-event @testing-library/dom


user-event tries to simulate the real events that would happen in the browser as the user interacts with it. For example userEvent.click(checkbox) would change the state of the checkbox.

fireEvent와 마찬가지로 유저 상호작용을 컴포넌트에서 테스트하기 위해 제공되는 함수로 거의 똑같이 동작하지만(내부적으로 fireEvent로 구현됨)

실제 사용자가 수행하는 것과 보다 유사하게 이벤트를 발생시킨다고 한다.

user-event is a companion library for Testing Library that simulates user interactions by dispatching the events that would happen if the interaction took place in a browser.


fireEvent vs userEvent


전체적인 상호작용을 테스트한다

fireEvent는 프로그램적으로 이벤트를 발생시킨다.

click()을 호출한다면 onClick 이라는 Dom Event를 발생시키는 것이다.

userEvent는 전체적인 상호작용을 테스트할 수 있다.

click()을 호출한다면 클릭하기까지 발생할 수 있는 다양한 이벤트들을 거치게 된다.

브라우저에서 사용자가 상호작용할 때 지정된 이벤트만 호출하리라는 법은 없다.

react-testing-library로 테스트를 한다고 해도 실제로 눈에 보이는 것이 없고 브라우저 환경이 아니기 때문에 개발자가 지정한 이벤트만 모의해서 테스트하는 경우 실제 환경과 차이가 있을 수 있는데 이런점들을 최대한 보완해준 느낌인 것 같다.

예시:
사용자가 텍스트 상자에 입력할 때 요소에 focus 하는 이벤트가 발생하고 그 다음 키보드 및 입력 이벤트가 발생하고 입력할 때 요소의 선택 및 값이 조작된다.

userEvent click 내부 구현

function click(...) {
  if (!skipPointerEventsCheck && !hasPointerEvents(element)) {
    throw new Error(...)
  }
  // We just checked for `pointerEvents`. We can always skip this one in `hover`.
  if (!skipHover) hover(element, init, {skipPointerEventsCheck: true})

  if (isElementType(element, 'label')) {
    clickLabel(element, init, {clickCount})
  } else if (isElementType(element, 'input')) {
    if (element.type === 'checkbox' || element.type === 'radio') {
      clickBooleanElement(element, init, {clickCount})
    } else {
      clickElement(element, init, {clickCount})
    }
  } else {
    clickElement(element, init, {clickCount})
  }
}

userEventclick 이벤트 메소드인데 내부적으로 click을 하기 전 hover가 동작할 수 있음을 알 수 있다.


차이 확인하기

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

const Button = () => {
  return (
    <button
      onClick={() => console.log("click")}
      onMouseOver={() => console.log("hover")}
    >
      button
    </button>
  );
};

describe("Button Test", () => {
  beforeEach(() => {
    console.log = jest.fn();
  });
  it("fireEvent", () => {
    render(<Button />);
    fireEvent.click(screen.getByRole("button"));
    expect(console.log).not.toHaveBeenCalledWith("hover");
    expect(console.log).toHaveBeenCalledWith("click");
    expect(console.log).toHaveBeenCalledTimes(1);
  });

  it("userEvent", async () => {
    const user = userEvent.setup();
    render(<Button />);
    await user.click(screen.getByRole("button"));
    expect(console.log).toHaveBeenCalledWith("hover");
    expect(console.log).toHaveBeenCalledWith("click");
    expect(console.log).toHaveBeenCalledTimes(2);
  });
});

사용자 지향적으로 읽을 수 있는 코드

  it("fireEvent input", () => {
    render(<input type="text" onFocus={() => console.log("focus")} />);
    fireEvent.change(screen.getByRole("textbox"), {
      target: { value: "helo" },
    });
    expect(screen.getByRole("textbox")).toHaveValue("helo");
    expect(console.log).not.toHaveBeenCalled();
  });

  it("userEvent input", async () => {
    const user = userEvent.setup();
    render(<input type="text" onFocus={() => console.log("focus")} />);
    await user.type(screen.getByRole("textbox"), "helo");
    expect(screen.getByRole("textbox")).toHaveValue("helo");
    expect(console.log).toHaveBeenCalledWith("focus");
  });

fireEvent의 경우 onChange Dom Event를 발생시키는 것이기 때문에 테스트에서 target.value를 직접 수정해주고 있지만 userEvent의 경우 type이라는 제공되는 메소드를 통해 상호작용을 발생시킨다.

컴포넌트 테스트에서 실제로 개발자가 궁금한 건 사용자가 키보드로 값을 입력하면 어떻게 된다 이지 ChangeEvent가 발생한 엘리먼트 타겟의 값을 수정했을 때 어떻게 된다 가 아닌 것 같다.

알아보기 쉽고 작성하기 쉬울 뿐 아니라 좀 더 사용자스러운? 테스트 코드라고 할 수 있다.

react-testing-library를 모르는 사람이 봐도 직관적일 것이기 때문에 의사소통도 보다 원활해질 것이다.

덤으로 사용자가 키입력을 위해 input을 클릭해 focus이벤트가 호출되는 것도 시나리오에 포함되고 있다.


  it('Opener 외부를 클릭하면 Opener가 close 된다.', async () => {
    const mockClick = jest.fn();
    render(
      <div>
        <p onClick={mockClick}>hello</p>
        <DotMenuOpener name="name">
          <h3>inner</h3>
        </DotMenuOpener>
      </div>,
    );
    const dotButton = screen.getByRole('button', { name: /verticaldot/i });
    fireEvent.click(dotButton);
    const innerHeading = screen.getByRole('heading', { name: 'inner' });
    expect(innerHeading).toBeInTheDocument();
    const helloText = screen.getByText('hello');
    // mouseDown??
    fireEvent.mouseDown(helloText);
    expect(innerHeading).not.toBeInTheDocument();
  });

userEvent를 공부하면서 알게 된 하나의 userEvent를 사용하지 않아서 불편했던 경험을 찾게 되었다.

메뉴 컴포넌트로 외부를 클릭하면 메뉴가 사라지게 구성해둔 것을 테스트한 코드이다.

실제 코드 상에 mousedown 이벤트를 사용해 외부 클릭 이벤트를 처리했기 때문에 테스트 코드에서도 mouseDown 이벤트를 발생시킨 모습이다.

    const dotButton = screen.getByRole('button', { name: /verticaldot/i });
    await user.click(dotButton);
    const innerHeading = screen.getByRole('heading', { name: 'inner' });
    expect(innerHeading).toBeInTheDocument();
    const helloText = screen.getByText('hello');
    await user.click(helloText);
    expect(innerHeading).not.toBeInTheDocument();

테스트케이스 제목이자 목적인 외부를 클릭하면 닫힌다와 좀 더 가까워졌고

앱에서도 사용자가 외부를 클릭하면 닫히게 동작하기 때문에 좀 더 실제환경과 유사해졌다고 할 수 있지 않을까 생각한다.

fireEvent를 통해 useClickAway hook을 들여다보며 음~ mousedown 이벤트를 사용해 처리했군이라고 인식하여 테스트 코드를 작성했었다면

나는 사용자다 버튼을 클릭해 메뉴를 열고 외부를 클릭해 메뉴를 닫는다로 접근해 테스트코드를 작성하는 흐름으로 변경된 것이지 않을까 싶다.


그래서 무엇을 사용해야할까요?

대부분의 상황에서 사용자의 시점에서 이벤트를 테스트하고자 한다면 userEvent가 적절하다.

rtl 문서에서도 user-event를 권장한다.

컴포넌트에서 이벤트를 발생시키고 변경을 테스트하는 기본적인 모든 케이스에서는 user-event를 사용하는 것이 적합하다고 생각한다.

react-testing-library를 사용하는 목적 그 자체이기 때문에 해당 라이브러리에서도 추천하고 있지 않을까 생각한다.


커스텀 이벤트, 특정 상황에 대한 독립적인 시뮬레이션

  fireEvent(button, new Event('customEvent'));

커스텀한 이벤트를 직접 발생시켜 테스트하거나 특정 이벤트가 발생하는 시점에서의 로직을 테스트해야하는 상황에서는 fireEvent가 적합하다고 생각한다.

사용자 상호작용으로 인한 컴포넌트의 변경을 테스트하는 것이 아닌 이벤트 리스너를 사용하는 hook 같은 것을 단위 테스트하거나

어떤 이벤트가 이미 발생된 이후의 UI나 동작을 테스트할 때 적합하지 않을까 생각한다.


References

profile
강해지고 싶은 주니어 프론트엔드 개발자

0개의 댓글