Jest로 next component test하기

Next.js 환경의 실무 프로젝트에서 Report라는 페이지 컴포넌트를 처음 테스트하면서 겪었던 이슈들을 정리했습니다.

먼저 테스트 코드의 많은 예제에서 다루는 것처럼 테스트의 기본은 컴포넌트가 제대로 render되는지 확인하는 것이라 생각하여 render시키고 난 후에 다음 것들을 생각해보고자 하였습니다. 분명히 마음처럼 쉽게 되지 않을 거라는 것을 알았기 때문입니다.

import Report from '../components/TabContent/Report';
import { render } from '@testing-library/react';

describe('Report', () => {
	it('should render', () => {
		render(<Report />);
	});
});

바로 첫번째 이슈를 겪었습니다.

Jest useNavigate() may be used only in the context of a Router component

image

컴포넌트 내부에서 useNavigate()를 사용하고 있었는데 Router component로 감싸지 않아서 에러가 났습니다. Router component로 감싸서 해결했습니다.

import Report from '../components/TabContent/Report';
import { render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';

describe('Report', () => {
	it('should render', () => {
		render(
			<Router>
				<Report />
			</Router>,
		);
	});
});

이제는 react-redux 관련 에러가 났습니다.

could not find react-redux context value; please ensure the component is wrapped in a <Provider>

image

Writing Tests | Redux

관련 이슈를 검색해보니 redux를 test에서 사용할 때 가이드라인이 있어 해당 코드를 추가했습니다.

// src/utils/test-utils.tsx

import React, { PropsWithChildren } from 'react';
import { render } from '@testing-library/react';
import type { RenderOptions } from '@testing-library/react';
import { configureStore } from '@reduxjs/toolkit';
import type { PreloadedState } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
import type { AppStore, RootState } from '@/store';
import { reducers } from '@/store';

interface ExtendedRenderOptions extends Omit<RenderOptions, 'queries'> {
	preloadedState?: PreloadedState<RootState>;
	store?: AppStore;
}

export function renderWithProviders(
	ui: React.ReactElement,
	{
		preloadedState = {},
		store = configureStore({ reducer: reducers, preloadedState }),
		...renderOptions
	}: ExtendedRenderOptions = {},
) {
	function Wrapper({ children }: PropsWithChildren<{}>): JSX.Element {
		return <Provider store={store}>{children}</Provider>;
	}
	return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
}
import Report from '../components/TabContent/Report';
import { BrowserRouter as Router } from 'react-router-dom';
import { renderWithProviders } from '@/utils/test-utils'

describe('Report', () => {
	it('should render', () => {
		renderWithProviders(
			<Router>
				<Report />
			</Router>
		);
	});
});

다시 돌려보니까 새로운 에러를 발견했습니다. 검색해보니 아래와 같은 솔루션을 발견할 수 있었습니다.

window.matchMedia is not a function

image

Jest test fails : TypeError: window.matchMedia is not a function Manual Mocks · Jest

import Report from '../components/TabContent/Report';
import { BrowserRouter as Router } from 'react-router-dom';
import { renderWithProviders } from '@/utils/test-utils'

Object.defineProperty(window, 'matchMedia', {
	writable: true,
	value: jest.fn().mockImplementation(query => ({
		matches: false,
		media: query,
		onchange: null,
		addListener: jest.fn(), // Deprecated
		removeListener: jest.fn(), // Deprecated
		addEventListener: jest.fn(),
		removeEventListener: jest.fn(),
		dispatchEvent: jest.fn(),
	})),
});

describe('Report', () => {
	it('should render', () => {
		renderWithProviders(
			<Router>
				<Report />
			</Router>
		);
	});
});

image

다시 돌려봤는데 다른 에러가 발생했습니다. 하지만 비슷한 원인의 에러인거 같아서 더 찾아봤고, window가 아닌 global 객체의 matchMedia를 mocking해야 하는 상황이었습니다.

import Report from '../components/TabContent/Report';
import { BrowserRouter as Router } from 'react-router-dom';
import { renderWithProviders } from '@/utils/test-utils';

global.matchMedia =
	global.matchMedia ||
	function () {
		return {
			addListener: jest.fn(),
			removeListener: jest.fn(),
		};
	};

describe('Report', () => {
	it('should render', () => {
		renderWithProviders(
			<Router>
				<Report />
			</Router>,
		);
	});
});

드디어 정상적으로 작동했습니다 🙂

global.matchMedia를 mocking하는 부분은 공통적으로 실행될 수 있는 영역에다가 넣어야겠다는 생각이 들었습니다.


Written by@Donghoon Song
사람들의 꿈을 이어주는 코멘토에서 일하고 있습니다.

InstagramGitHubTwitterLinkedIn