검증하지 않아도 되는 것
의존성의 동작 같은 것
범용적인 라이브러리(ex. react
, react-router-dom
)는 이미 내부적으로 상세히 핵심 기능을 단위 통합 테스트로 검증한 상태
→ 우리가 useNavigate를 사용하는 컴포넌트의 단위 테스트에서 useNavigate의 동작까지 검증할 필요 無
검증해야 할 것
(홈으로 이동 링크와 같은 버튼을 눌렀을 때) UseNavigate에서 제공하는 API가 제대로 호출되었는지
단위 테스트로 검증하기 좋은 이유
상위에서 다른 컴포넌트들과 조합되어 사용 X && 독립적으로 동작
(여러 컴포넌트가 조합되어 있어도 기능 or 자식 컴포넌트가 단순함)
모두 react-router
에 의존성을 가지고 있고 useNavigate
란 훅을 사용
EmptyNotice.jsx
...생략
import { useNavigate } from 'react-router-dom';
const EmptyNotice = () => {
const navigate = useNavigate();
const handleClickBack = () => {
navigate(pageRoutes.main);
};
return (
<Box>
<Typography>
텅~
</Typography>
<MuiLink
onClick={handleClickBack}
>
홈으로 가기
</MuiLink>
</Box>
);
};
export default EmptyNotice;
EmptyNotice.spec.jsx
import { screen } from '@testing-library/react';
import React from 'react';
import EmptyNotice from '@/pages/cart/components/EmptyNotice';
import render from '@/utils/test/render';
// 실제 모듈을 모킹한 모듈로 대체하여 테스트 실행
// useNavigate 훅으로 반환받은 navigate함수가 올바르게 호출되었는가 -> 스파이 함수
const navigateFn = vi.fn(); // 스파이 함수
vi.mock('react-router-dom', async () => {
const original = await vi.importActual('react-router-dom');
return { ...original, useNavigate: () => navigateFn };
});
it('"홈으로 가기" 링크를 클릭할경우 "/"경로로 navigate함수가 호출된다', async () => {
const { user } = await render(<EmptyNotice />); // EmptyNotice 컴포넌트 렌더링
await user.click(screen.getByText('홈으로 가기')); // 홈으로 가기 링크 버튼 클릭
expect(navigateFn).toHaveBeenNthCalledWith(1, '/');
// useNavigate에서 반환하는 navigateFn이 우리가 원하는 인자와 호출되는지 단언
});
vi.mock
실제 모듈을 모킹한 모듈로 대체하여 테스트를 실행하게 하는 것
특정 모듈을 원하는 형태로 대체 가능
테스트를 실행 시, import된 모듈을 가져오기 전에 실행됨
react-router-dom 라이브러리 모킹
vi.mock('react-router-dom', async () => {
// 실제 react-router-dom에 있는 모듈을 그대로 가져옴
const original = await vi.importActual('react-router-dom');
return { ...original, useNavigate: () => navigateFn };
// useNavigate 자체를 spy함수로 모킹하여 react-router-dom 모듈의 기존 구현을 대체 가능
});
모킹할 모듈의 이름 작성
원하는 대체 구현을 함수 형태로 작성
vi.importActual
일부 모듈만 모킹 (나머지는 기존 모듈의 기능을 그대로 사용)
const navigateFn = vi.fn();
스파이 함수
효과
react-router-dom의 다른 모듈에 대한 불필요한 모의구현 없이 편리하게 모킹하여 테스트 실행 가능
결론
외부 모듈과의 의존성이 있는 경우, spy 함수를 사용해서 특정 구현을 모킹하고 우리가 원하는 인자와 함께 올바르게 호출되었는지만 검증하면 독립적으로 단위 테스트 작성 가능
검증할 것
뒤로 이동 버튼을 클릭했을 때 navigate로 -1인자가 올바르게 전달되어 호출되는지 확인
ErrorPage.jsx
import { useNavigate } from 'react-router-dom';
export const ErrorPage = () => {
const navigate = useNavigate();
const handleClickBackButton = () => {
navigate(-1); // navigate로 마이너스 1 인자를 전달하여 호출
};
return (
<div>
<h1>읔!</h1>
<p>예상치 못한 에러가 발생했습니다.</p>
<button onClick={handleClickBackButton}>뒤로 이동</button>
</div>
);
};
ErrorPage.spec.jsx
import { screen } from '@testing-library/react';
import ErrorPage from '@/pages/error/components/ErrorPage';
import render from '@/utils/test/render';
// 4. 페이지 이동이 정상적으로 실행되는지 확인하기 위해 react-router-dom의
// useNavigate를 spy함수로 모킹
const navigateFn = vi.fn();
vi.mock('react-router-dom', async () => {
const original = await vi.importActual('react-router-dom');
return { ...original, useNavigate: () => navigateFn };
});
it('"뒤로 이동" 버튼 클릭시 뒤로 이동하는 navigate(-1) 함수가 호출된다', async () => {
const { user } = await render(<ErrorPage />);
// 1. 에러 페이지 컴포넌트를 렌더링
const button = await screen.getByRole('button', { name: '뒤로 이동' });
// 2. 뒤로 이동 버튼을 조회
// (name 지정 시, 버튼 중에서도 뒤로 이동이란 텍스트를 가진 요소를 명확하게 지정 가능)
await user.click(button); // 3. 해당 버튼을 클릭
expect(navigateFn).toHaveBeenNthCalledWith(1, -1);
// 5. 스파이 함수가 원하는 인자와 함께 호출되는지만 단언
});
검증할 것
react-router-dom에 useNavigate를 spy함수로 모킹한 후 특정 인자와 함께 올바르게 호출되었는지 검증
NotFoundPage.jsx
import { useNavigate } from 'react-router-dom';
import { pageRoutes } from '@/apiRoutes';
export const NotFoundPage = () => {
const navigate = useNavigate();
const handleClickNavigateHomeButton = () => {
navigate(pageRoutes.main, { replace: true }); // 👈
};
return (
<div id="error-page">
<h1>404</h1>
<p>페이지 경로가 잘못 되었습니다!</p>
<button onClick={handleClickNavigateHomeButton}>Home으로 이동</button>
</div>
);
};
export default NotFoundPage;
NotFoundPage.spec.jsx
import { screen } from '@testing-library/react';
import NotFoundPage from '@/pages/error/components/NotFoundPage';
import render from '@/utils/test/render';
const navigateFn = vi.fn();
vi.mock('react-router-dom', async () => {
const original = await vi.importActual('react-router-dom');
return { ...original, useNavigate: () => navigateFn };
});
it('Home으로 이동 버튼 클릭시 홈 경로로 이동하는 navigate가 실행된다', async () => {
const { user } = await render(<NotFoundPage />);
// 1. Not Found 페이지를 렌더링
const button = await screen.getByRole('button', { name: 'Home으로 이동' });
// 2. 홈으로 이동 버튼을 찾음
await user.click(button);
// 3. 유저 이벤트를 사용하여 클릭 이벤트를 시뮬레이션
expect(navigateFn).toHaveBeenNthCalledWith(1, '/', { replace: true });
// 4. 모킹한 navigateFn의 메인 루트 경로와 { replace: true } 라는 옵션이 명확하게 전달되었는지 단언
});
결과적으로 이러한 영향은 테스트의 신뢰성을 떨어뜨릴 수 있음
→ 테스트 실행 후, 특정 모드를 모킹했을 때 반드시 항상 모킹 작업을 초기화하는 것이 좋음
...생략
afterEach(() => {
server.resetHandlers();
// 모킹된 모의 객체 호출에 대한 히스토리를 초기화
vi.clearAllMocks(); // 👈 1
});
afterAll(() => {
// 모킹 모듈에 대한 모든 구현을 초기화
vi.resetAllMocks(); // 👈 2
server.close();
});
...생략
vi.clearAllMocks()
모킹된 모의 객체 호출에 대한 히스토리를 초기화
먼저 각 테스트가 종료된 후에 afterEach teardown에서 vi.clearAllMocks
함수를 호출하여 모킹된 모의객체 호출에 대한 히스토리를 초기화함.
모킹된 모의객체의 구현 자체는 제거되지 않음.
기존 모듈이 모킹된 상태 그대로 유지
유지 이유 : 사전에 작성한 테스트가 올바르게 실행되기 때문
모킹 히스토리를 계속 쌓아두면
spy 함수의 호출 횟수/인자가 계속 바뀌어 다른 테스트에 영향 줄 가능성 有
→ 그래서 테스트 실행이 끝날 때마다 clearAllMocks 함수를 호출하여 히스토리를 항상 초기화함.
vi.resetAllMocks()
모킹 모듈에 대한 모든 구현을 초기화
모든 테스트가 종료된 후, afterAll teardown에서 mocking 모듈이 더 이상 의미가 없기 때문에 vr.resetAllMocks 함수를 호출하여 spy함수에 대한 호출 횟수/결과뿐만 아니라 mocking 모듈에 대한 모든 구현을 초기화함.
setup.js 파일
setup.js 파일에 작성한 이유
모든 테스트가 끝날 때마다 mocking 초기화를 하기 위해 teardown 작업을 전역에서 동작하도록 함.
호과
모든 테스트의 독립성 & 안정성을 보장 가능 (다른 테스트에서 실행한 모킹 작업에 영향받지 않도록)
공식 문서를 참고하여 적절한 API 확인하기
vi.restoreAllMocks
와 같은 API도 있음
모의객체 호출에 대한 히스토리는 초기화하지만 모킹 모듈의 구현을 원래 모듈의 로직으로 되돌림
참고