본문 바로가기
SPA/React

[React] Jest 이벤트 처리 비교, fireEvent vs userEvent

by J4J 2023. 11. 2.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 Jest 이벤트 처리를 위해 사용되는 fireEvent와 userEvent를 비교해 보는 시간을 가져보려고 합니다.

 

 

 

Jest 이벤트 처리란?

 

jest를 이용하여 테스트 케이스를 작성하다보면 특정 element에 이벤트가 발생되었을 때 어떤 상황이 만들어져야 하는지에 대한 경우도 생각해 볼 수 있습니다.

 

예를 들면, 버튼을 클릭했더니 화면에 보이지 않던 컴포넌트가 보이기 시작한다거나 체크박스를 클릭했더니 실제로 체크가 되었는지 등에 대한 것들을 말해볼 수 있습니다.

 

 

 

이런 이벤트 처리를 수행하기 위해 jest에서 활용해볼 수 있는 모듈은 크게 2가지로 fireEvent와 userEvent가 있습니다.

 

fireEvent와 userEvent를 이용하여 처리할 수 있는 이벤트들은 거의 유사하다고 볼 수 있습니다.

 

일단 여기서 말하고자 하는 것은 어떤 기능을 제공해주는지에 대한 것이 아닙니다.

 

만약 사용할 수 있는 이벤트들이 어떤 것들이 있는지를 자세하고 알고 싶으신 분들은 testing-library 공식 문서 (fireEvent)testing-library 공식 문서 (userEvent)를 참고해주시면 좋을 것 같습니다.

 

 

 

fireEvent와 userEvent의 가장 큰 차이점은 testing-library 공식 문서 (fireEvent, userEvent difference)을 보시면 확인할 수 있는 것처럼 user interaction의 여부입니다.

 

여기서 말하는 user interaction은 사용자가 브라우저에 접근하여 서비스와 상호작용 하는 것을 의미합니다.

 

즉, userEvent를 사용하게 되면 모든 interaction이 동시에 발생이 되는데 이것은 사용자가 실제로 이벤트를 발생시켰을 때 생길 수 있는 상황과 동일한 것을 의미합니다.

 

하지만 fireEvent를 사용하게 되면 사용자가 실제로 이벤트를 발생시켰을 때 생길 수 있는 상황이 아닌 fireEvent를 통해 발생시키고자 하는 이벤트만 실행되는 결과를 만듭니다.

 

조금 더 자세한 내용에 대해 알기 위해 코드로 간단히 표현해 보겠습니다.

 

 

반응형

 

 

User Interaction 비교

 

테스트를 위해 간단하게 다음과 같이 소스 코드를 작성해 봤습니다.

 

import { useState } from 'react';

export default function Interaction() {
    const [isClick, setIsClick] = useState<boolean>(false);
    const [isHover, setIsHover] = useState<boolean>(false);

    return (
        <main>
            <button data-testid="button" onClick={() => setIsClick(!isClick)} onMouseOver={() => setIsHover(!isHover)}>
                button
            </button>

            {isClick && <h2 data-testid="click-text">Click !</h2>}
            {isHover && <h2 data-testid="hover-text">Hover !</h2>}
        </main>
    );
}

 

 

 

그리고 서버를 실행하여 다음과 같이 버튼을 클릭해 보면 click과 hover가 동시에 발생되는 것을 확인할 수 있습니다.

 

사용자 입장 버튼 클릭 결과

 

 

 

위의 상황에 대해 테스트 케이스를 작성해 본다고 가정하겠습니다.

 

그러면 일반적으로 생각해 볼 수 있는 테스트 코드는 다음과 같이 작성될 것으로 보입니다.

 

import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Interaction from './interaction';

describe('interaction test', () => {
    test('fireEvent test', async () => {
        render(<Interaction />);
        fireEvent.click(screen.getByTestId('button'));

        expect(await screen.findByTestId('click-text')).toBeInTheDocument();
        expect(await screen.findByTestId('hover-text')).toBeInTheDocument();
    });

    test('userEvent test', async () => {
        render(<Interaction />);
        userEvent.click(screen.getByTestId('button'));

        expect(await screen.findByTestId('click-text')).toBeInTheDocument();
        expect(await screen.findByTestId('hover-text')).toBeInTheDocument();
    });
});

 

 

하지만 결과를 확인해 보면 userEvent만 통과하고 fireEvent는 통과하지 못하는 것을 볼 수 있습니다.

 

interaction 테스트 결과

 

 

 

 

이런 결과가 나오는 이유는 위에서 말한 것처럼 userEvent만 모든 interaction이 동시에 발생되기 때문입니다.

 

테스트 코드를 확인해 보면 fireEvent와 userEvent 모두 click 이벤트만 발생을 시켰는데, fireEvent 같은 경우는 오직 발생시킨 click 이벤트에 대해서만 처리가 이루어집니다.

 

하지만 userEvent는 click 이벤트를 발생시킬 때 함께 동작될 수 있는 hover와 같은 이벤트들도 함께 실행되기 때문에 click 뿐만 아니라 다른 이벤트 처리에 대한 결과도 확인할 수 있습니다.

 

즉, fireEvent까지 테스트를 통과하고 싶다면 테스트 코드는 다음과 같이 변경될 필요가 있습니다.

 

import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Interaction from './interaction';

describe('interaction test', () => {
    test('fireEvent test', async () => {
        render(<Interaction />);

        fireEvent.click(screen.getByTestId('button'));
        expect(await screen.findByTestId('click-text')).toBeInTheDocument();

        fireEvent.mouseOver(screen.getByTestId('button'));
        expect(await screen.findByTestId('hover-text')).toBeInTheDocument();
    });

    test('userEvent test', async () => {
        render(<Interaction />);
        userEvent.click(screen.getByTestId('button'));

        expect(await screen.findByTestId('click-text')).toBeInTheDocument();
        expect(await screen.findByTestId('hover-text')).toBeInTheDocument();
    });
});

 

수정된 interaction 테스트 결과

 

 

 

이런 부분들을 확인해보면 어떤 상황에서 fireEvent와 userEvent를 각각 사용해야 하는지 생각해 볼 수 있습니다.

 

먼저 fireEvent는 발생시킨 이벤트에 대한 결과만 확인할 수 있기 때문에 개별적인 이벤트 처리에 대한 테스트가 필요할 때 활용해 볼 수 있습니다.

 

그리고 userEvent는 사용자가 이벤트를 발생시킬 때와 동일한 결과를 만들어내기 때문에 사용자 입장에서 테스트가 필요할 때 활용해볼 수 있습니다.

 

결과적으로 특별한 케이스가 없다면 사용자 입장에서 동일한 결과를 만드는 테스트를 작성하는 것이 더 효과적일 것이라고 판단할 수 있기에 대부분의 개발자분들은 fireEvent보단 userEvent를 더 많이 활용할 것으로 생각합니다.

 

 

 

 

Change 이벤트 비교

 

fireEvent와 userEvent의 또 다른 쉽게 볼 수 있는 차이점은 change 이벤트가 있습니다.

 

change 이벤트를 위한 테스트 케이스를 작성할 때 두 모듈의 가장 큰 차이점은 실제 타이핑 결과와 동일 여부입니다.

 

이미 위에서 userEvent는 사용자 관점에서 동작된다는 것을 봤기 때문에 여기서도 사용자가 접속했을 때 실제 타이핑한 결과와 동일하게 나오는 것은 userEvent라고 생각해 볼 수 있습니다.

 

그러면 반대로 fireEvent에 대해서도 알아봐야 하는데, fireEvent는 userEvent와 다르게 change 이벤트가 발생될 때 타이핑을 하는 상황이 아닌 변경되는 value 값 자체를 넣는 작업을 수행합니다.

 

 

 

이 또한 간단하게 다음과 같이 작성된 코드를 확인하여 비교해 보겠습니다.

 

import { useState } from 'react';

export default function Change() {
    const [data, setData] = useState<string>('');

    return (
        <main>
            <input data-testid="input" type="text" onChange={(e) => setData(e.target.value)} />
            <h2 data-testid="data-text">{data}</h2>
        </main>
    );
}

 

사용자 입장 change 이벤트 발생

 

 

 

위의 상황은 사용자가 서버에 접속하여 타이핑을 친 것으로 해당 케이스에 대해 테스트 코드를 작성해 보면 다음과 같이 작성해 볼 수 있습니다.

 

import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Change from './change';

describe('change test', () => {
    test('fireEvent test', () => {
        render(<Change />);

        const first = 'first';
        fireEvent.change(screen.getByTestId('input'), {
            target: {
                value: first,
            },
        });

        const second = 'second';
        fireEvent.change(screen.getByTestId('input'), {
            target: {
                value: second,
            },
        });

        expect(screen.getByTestId('data-text').textContent).toEqual(first.concat(second));
    });

    test('userEvent test', async () => {
        render(<Change />);

        const first = 'first';
        await userEvent.type(screen.getByTestId('input'), first);

        const second = 'second';
        await userEvent.type(screen.getByTestId('input'), second);

        expect(screen.getByTestId('data-text').textContent).toEqual(first.concat(second));
    });
});

 

 

 

 

그리고 테스트 결과를 확인해 보면 다음과 같이 userEvent만 통과하는 것도 볼 수 있습니다.

 

change 이벤트 테스트 결과

 

 

 

이런 결과가 나오는 이유는 위에서 언급한 것처럼 실제 사용자가 타이핑 치는 것과 동일한 상황을 만드는 것은 userEvent이기 때문입니다.

 

그리고 fireEvent는 타이핑을 친다는 느낌보다는 value 자체를 덮어써버리는 것이기에 fireEvent에 대해서도 테스트가 통과되고 싶다면 다음과 같이 작성되는 것이 올바릅니다.

 

import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Change from './change';

describe('change test', () => {
    test('fireEvent test', () => {
        render(<Change />);

        const first = 'first';
        fireEvent.change(screen.getByTestId('input'), {
            target: {
                value: first,
            },
        });

        const second = 'second';
        fireEvent.change(screen.getByTestId('input'), {
            target: {
                value: (screen.getByTestId('input') as HTMLInputElement).value.concat(second),
            },
        });

        expect(screen.getByTestId('data-text').textContent).toEqual(first.concat(second));
    });

    test('userEvent test', async () => {
        render(<Change />);

        const first = 'first';
        await userEvent.type(screen.getByTestId('input'), first);

        const second = 'second';
        await userEvent.type(screen.getByTestId('input'), second);

        expect(screen.getByTestId('data-text').textContent).toEqual(first.concat(second));
    });
});

 

수정된 change 이벤트 테스트 결과

 

 

 

 

 

 

 

 

이상으로 Jest 이벤트 처리를 위해 사용되는 fireEvent와 userEvent를 비교해 보는 것에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글