안녕하세요. 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');
});
});
위의 코드를 살펴보면 컴포넌트의 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');
});
});
위의 예시는 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');
});
});
테스트를 수행하기 전 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.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에서 호출되는 함수 mocking 하는 방법에 대해 간단하게 알아보는 시간이었습니다.
읽어주셔서 감사합니다.
'SPA > React' 카테고리의 다른 글
[React] SSE (Server-Sent Events) 사용하여 실시간 통신하기 (0) | 2024.05.08 |
---|---|
[React] Jest에서 path alias 적용하기 (0) | 2024.01.18 |
[React] Jest에서 render 커스텀하여 전역 wrapper 설정하기 (2) | 2024.01.14 |
[React] Zod를 이용하여 React Hook Form 사용하기 (0) | 2023.12.24 |
[React] react-query v5 변경점 알아보기 (5) - useMutation 기능 변경 (0) | 2023.12.13 |
댓글