userEvent
, 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 하면 이벤트가 발생하고 실제 컴포넌트에서 작성된 이벤트에 대한 변화를 확인할 수 있다.
의존성
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
는 프로그램적으로 이벤트를 발생시킨다.
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})
}
}
userEvent
의 click
이벤트 메소드인데 내부적으로 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나 동작을 테스트할 때 적합하지 않을까 생각한다.
깔끔하게 정리된 글 감사합니다. fireEvent와 userEvent 차이를 명확하게 알 수 있게 되었네요.