본문 바로가기
SPA/React

[React] Jest에서 호출되는 함수 mocking 하기

by J4J 2024. 1. 20.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 jest에서 호출되는 함수 mocking 하는 방법에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

Jest에서 Mocking이 필요한 이유

 

mock이란 번역을 했을 때 확인할 수 있는 것처럼 모조품, 가짜라는 의미를 가지고 있습니다.

 

그래서 mocking이라는 것은 mock 데이터를 활용하는 것으로 해석해볼 수 있으며 주로 테스트 케이스를 작성할 때 mock 데이터를 활용하여 테스트 코드를 작성하는 것을 의미합니다.

 

 

반응형

 

 

jest에서 mock 데이터가 필요한 이유는 간단합니다.

 

jest에 의해 테스트될 수 있는 다양한 모듈 및 컴포넌트들에는 다양한 함수들이 존재할 수 있고 한 가지의 함수가 올바르게 동작되는지 테스트할 때 함수에 의존된 여러 가지 함수들이 실행될 수도 있습니다.

 

예를 들면 다음과 같은 경우가 될 수 있습니다.

 

// sample case
function function1() {
   functon2(); // 0.2s
   functon3(); // 1.2s
   functon4(); // 0.8s
   
   {function1 핵심 로직 동작} // 0.3s
}

 

 

 

function1의 핵심 로직에 대해서만 테스트를 해보고 싶은데 이를 확인하기 위해서는 function2, function3, function4의 동작이 이루어져야 합니다.

 

핵심 로직만 생각해 보면 0.3s의 시간이 필요하지만 의존된 함수들의 시간까지 고려하면 2.5s의 시간이 필요하기에 더 많은 리소스를 요구하게 됩니다.

 

그래서 이런 경우 jest가 실행될 때만 다음과 같이 만들어 볼 수 있습니다.

 

// sample case
function function1() {
   functon2(); // mocking, 0.01s
   functon3(); // mocking, 0.01s
   functon4(); // mocking, 0.01s
   
   {function1 핵심 로직 동작} // 0.3s
}

 

 

 

 

 핵심 로직을 제외한 함수들을 mock 데이터로 변경함으로써 실제로 function2, function3, function4의 동작이 이루어지는 것 없이 function1의 핵심 로직만 테스트할 수 있게 됩니다.

 

또한 다른 말로는 한 가지의 테스트를 수행할 때 function2, function3, function4의 환경까지 고려할 필요가 없기 때문에 특정 목적만을 위한 테스트에 더 집중해 볼 수 있습니다.

 

즉, 프로젝트의 상황에 따라 수 십 개, 수 백개 이상의 테스트가 작성될 수 있는 jest에서 mocking을 잘 활용하게 된다면 더 많은 비용을 지불하지 않고 모든 테스트를 수행해 볼 수 있는 결과를 만들어 볼 수 있습니다.

 

 

 

jest에서 mocking을 하기 위해 사용될 수 있는 다양한 기능들이 있습니다.

 

그중 대표적인 것들에 대해서만 소개를 해보도록 하겠습니다.

 

 

 

 

jest.fn()

 

jest.fn()은 jest 내부에서 동작되는 1가지의 함수를 mock 함수로 변경을 하기 위해 사용됩니다.

 

함수에 대한 mock 데이터를 생성할 때 활용하는 것으로 다음과 같이 코드에 적용해 볼 수 있습니다.

 

// src/PropsFunction.tsx
import { useState } from 'react';

interface Props {
    onPropsFunction?: () => void;
}

export default function PropsFunction({ onPropsFunction }: Props) {
    const [hasPropsFunction, setHasPropsFunction] = useState<boolean>(false);

    const handleCheckPropsFunctionClick = () => {
        if (onPropsFunction) {
            setHasPropsFunction(true);
        }
    };

    return (
        <div>
            <h2>Mock Test</h2>
            <div>
                <p role="has-props-function-text">{String(hasPropsFunction)}</p>

                <button role="check-props-function-button" type="button" onClick={handleCheckPropsFunctionClick}>
                    check
                </button>
            </div>
        </div>
    );
}


// src/PropsFunction.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import PropsFunction from './PropsFunction';

describe('props function test', () => {
    test('jest fn not use test', async () => {
        render(<PropsFunction />);
        await userEvent.click(screen.getByRole('check-props-function-button'));

        expect((await screen.findByRole('has-props-function-text')).textContent).toEqual('false');
    });

    test('jest fn use test', async () => {
        const onPropsFunction = jest.fn(); // onPropsFunction mock 함수 생성

        render(<PropsFunction onPropsFunction={onPropsFunction} />);
        await userEvent.click(screen.getByRole('check-props-function-button'));

        expect((await screen.findByRole('has-props-function-text')).textContent).toEqual('true');
    });
});

 

 

 

 

jest.fn() 사용 예제 테스트

 

 

 

위의 코드를 살펴보면 컴포넌트의 props에 전달된 함수가 있는지 여부에 따라 text의 값이 바뀌는 것을 볼 수 있습니다.

 

수행되는 테스트에 props로 전달되는 함수가 무엇인지 궁금하지 않다면 위처럼 로직이 정의된 함수를 입력하여 props로 전달하지 않고 jest.fn()을 이용한 mock 함수를 전달해 볼 수 있습니다.

 

 

 

 

jest.mock()

 

jest.fn()이 함수에 대한 mock 데이터를 생성하는 것이라면 jest.mock()은 해당 함수들을 가지고 있는 모듈에 대해 mock 데이터를 생성하도록 도와줍니다.

 

모듈이라고 하는 것은 단순하게 얘기하면 import/require 등을 이용하여 호출하는 대상들로 이해해 주시면 됩니다.

 

 

 

 

jest.mock()은 모듈을 대상으로 mock 데이터를 생성하는 것이기 때문에 jest.mock()을 사용할 때는 jest.fn()을 활용하여 내부 함수에 대한 정의를 도울 수 있습니다.

 

간단한 예시로 다음과 같은 코드가 작성될 수 있습니다.

 

// src/util.ts
export const getAddNumber = (number: number) => {
    return number + 2;
};

export default {
    getAddNumber,
};


// src/Number.tsx
import { useState } from 'react';
import { getAddNumber } from './util';

export default function Number() {
    const [number, setNumber] = useState<number>(1);

    const handleAddClick = () => {
        setNumber(getAddNumber(number));
    };

    return (
        <main>
            <h2>Mock Test</h2>
            <div>
                <p role="number-text">{number}</p>

                <button role="add-button" type="button" onClick={handleAddClick}>
                    add
                </button>
            </div>
        </main>
    );
}


// src/Number.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Number from './Number';

describe('number test', () => {
    test('jest mock not use test', async () => {
        render(<Number />);
        await userEvent.click(screen.getByRole('add-button'));

        expect((await screen.findByRole('number-text')).textContent).toEqual('3');
    });
});

 

jest.mock() 사용 전 테스트

 

 

 

 

위의 예시는 Number 컴포넌트에서 util이라는 모듈을 이용하여 내부 기능을 담고 있습니다.

 

jest.mock()을 활용하여 mocking을 하지 않는 다면 add button을 클릭했을 때 util에 정의된 대로 2라는 숫자 값만 증가하게 됩니다.

 

하지만 jest.mock()을 활용하여 mocking을 하게 된다면 다음과 같이 기존 util에 정의된 대로 동작이 이루어지지 않는 것을 볼 수 있습니다.

 

// src/Number.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Number from './Number';

// util 모듈에 담겨 있는 함수들을 mocking하여 재 정의
jest.mock('./util', () => {
    return {
        getAddNumber:
            // mock 함수로 변경
            jest
                .fn()
                // mock 함수에 담기는 함수 기능 새롭게 정의
                .mockImplementation((number: number) => number + 3),
    };
});

describe('number test', () => {
    test('jest mock use test', async () => {
        render(<Number />);
        await userEvent.click(screen.getByRole('add-button'));

        expect((await screen.findByRole('number-text')).textContent).toEqual('4');
    });
});

 

jest mock() 사용 후 테스트

 

 

 

테스트를 수행하기 전 util에 담겨 있는 함수가 jest.mock()에 의해 재 정의되었기 때문에 Number 컴포넌트에서 해당 함수를 호출하여 사용하는 경우 다른 결과가 나오는 것을 볼 수 있습니다.

 

 

 

 

jest.spyOn()

 

jest.spyOn()은 spy라는 이름에 맞게 실행되는 함수를 추적하기 위한 용도로 사용됩니다.

 

함수가 어떤 결과를 만드는지가 궁금하다기보다는 함수가 동작이 되었는지에 대해서만 궁금한 경우 jest.spyOn()을 활용해 볼 수 있습니다.

 

간단한 예시를 다음과 같이 작성해 볼 수 있습니다.

 

// src/util.ts
export const getAddNumber = (number: number) => {
    return number + 2;
};

export default {
    getAddNumber,
};


// src/Number.tsx
import { useState } from 'react';
import { getAddNumber } from './util';

export default function Number() {
    const [number, setNumber] = useState<number>(1);

    const handleAddClick = () => {
        setNumber(getAddNumber(number));
    };

    return (
        <main>
            <h2>Mock Test</h2>
            <div>
                <p role="number-text">{number}</p>

                <button role="add-button" type="button" onClick={handleAddClick}>
                    add
                </button>
            </div>
        </main>
    );
}


// src/Number.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Number from './Number';
import * as util from './util'; // import의 경우 asterisk를 활용하여 전체 함수를 불러와야 spyOn 동작

describe('number test', () => {
    test('jest spy use test', async () => {
        const getAddNumberSpy = jest.spyOn(util, 'getAddNumber'); // util 모듈에 있는 getAddNumber 함수 실행 추적

        render(<Number />);
        await userEvent.click(screen.getByRole('add-button'));
        await userEvent.click(screen.getByRole('add-button'));

        await waitFor(() => {
            expect(getAddNumberSpy).toHaveBeenCalled(); // 1번 이상 호출되었는지 확인
            expect(getAddNumberSpy).toHaveBeenCalledTimes(2); // 총 2번 호출되었는지 확인
        });
    });
});

 

jest.spyOn() 사용 예제 테스트

 

 

 

 

위의 코드 같은 경우 jest.fn(), jest.mock() 등과 같이 기존 함수에 대해 mock 데이터를 따로 정의하지 않고 실제 동작되는 함수에 대해 추적을 한 경우입니다.

 

하지만 반대로 mock 데이터를 생성한 경우에 대해서도 추적을 할 수 있기 때문에 두 가지 경우에 대해 모두 활용해 볼 수 있다는 점 참고하시면 됩니다.

 

 

 

 

추가적으로 jest.spyOn()은 단순히 추적 관련에 대해서만 사용되지 않고 jest.fn(), jest.mock()과 같이 기존 정의되어 있는 함수를 재 정의하여 테스트하는 코드도 작성할 수 있습니다.

 

위에서 jest.mock()을 활용하여 getAddNumber 함수를 재 정의했던 것과 동일한 코드를 다음과 같이 작성해 볼 수 있습니다.

 

// src/util.ts
export const getAddNumber = (number: number) => {
    return number + 2;
};

export default {
    getAddNumber,
};


// src/Number.tsx
import { useState } from 'react';
import { getAddNumber } from './util';

export default function Number() {
    const [number, setNumber] = useState<number>(1);

    const handleAddClick = () => {
        setNumber(getAddNumber(number));
    };

    return (
        <main>
            <h2>Mock Test</h2>
            <div>
                <p role="number-text">{number}</p>

                <button role="add-button" type="button" onClick={handleAddClick}>
                    add
                </button>
            </div>
        </main>
    );
}


// src/Number.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Number from './Number';
import * as util from './util'; // import의 경우 asterisk를 활용하여 전체 함수를 불러와야 spyOn 동작

describe('number test', () => {
    test('jest spy mocking test', async () => {
        // util 모듈에 있는 getAddNumber 함수 실행 추적하며 mock 함수 생성
        jest.spyOn(util, 'getAddNumber')
            // mock 함수에 담기는 함수 기능 새롭게 정의
            .mockImplementation((number: number) => number + 3);

        render(<Number />);
        await userEvent.click(screen.getByRole('add-button'));

        expect((await screen.findByRole('number-text')).textContent).toEqual('4');
    });
});

 

jest.spyOn() mocking 사용 예제 테스트

 

 

 

 

 

 

 

 

이상으로 jest에서 호출되는 함수 mocking 하는 방법에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글