これはなに
jestでのテスト時に、next/navigationのhooks (useRouter, usePathname, useSearchParams)のモックで困っている人が一定数いそうな気がしたので、解決方法を書いてみます。
結論
こんな感じでモックしたプロバイダーを作って
// app-router-context-provider-mock.tsx
import {
AppRouterContext,
AppRouterInstance,
} from 'next/dist/shared/lib/app-router-context.shared-runtime';
import React from 'react';
type AppRouterContextProviderMockProps = {
router?: Partial<AppRouterInstance>;
children: React.ReactNode;
};
export const AppRouterContextProviderMock = ({
router,
children,
}: AppRouterContextProviderMockProps): React.ReactNode => {
const mockedRouter: AppRouterInstance = {
back: jest.fn(),
forward: jest.fn(),
push: jest.fn(),
replace: jest.fn(),
refresh: jest.fn(),
prefetch: jest.fn(),
...router,
};
return (
<AppRouterContext.Provider value={mockedRouter}>
{children}
</AppRouterContext.Provider>
);
};
// pathname-context-provider-mock.tsx
import { PathnameContext } from 'next/dist/shared/lib/hooks-client-context.shared-runtime';
import { ReactNode } from 'react';
type PathNameContextProviderMockProps = {
pathname: string;
children: ReactNode;
};
export const PathNameContextProviderMock = ({
pathname,
children,
}: PathNameContextProviderMockProps): ReactNode => {
return (
<PathnameContext.Provider value={pathname}>
{children}
</PathnameContext.Provider>
);
};
// search-params-context-provider-mock.tsx
import { SearchParamsContext } from 'next/dist/shared/lib/hooks-client-context.shared-runtime';
type SearchParamsContextProviderMockProps = {
children: React.ReactNode;
searchParams: URLSearchParams;
};
export const SearchParamsContextProviderMock = ({
searchParams,
children,
}: SearchParamsContextProviderMockProps) => {
return (
<SearchParamsContext.Provider value={searchParams}>
{children}
</SearchParamsContext.Provider>
);
};
テストで使えば良いです。
// hoge.spec.tsx
import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime';
import { AppRouterContextProviderMock } from './app-router-context-provider-mock';
import { PathNameContextProviderMock } from './pathname-context-provider-mock';
import { SearchParamsContextProviderMock } from './search-params-context-provider-mock';
import { render, screen } from '@testing-library/react';
type Props = {
push: AppRouterInstance["push"];
pathname: string;
searchParams: URLSearchParams;
}
describe('なんかのテスト', () => {
const WrappedElement = ({push, pathname, searchParams}: Props) => (
<AppRouterContextProviderMock router={{push}}>
<PathNameContextProviderMock pathname={pathname}>
<SearchParamsContextProviderMock searchParams={searchParams}>
<テストしたいコンポーネント />
</SearchParamsContextProviderMock>
</PathNameContextProviderMock>
</AppRouterContextProviderMock>
);
it('should render correctly', () => {
const push = jest.fn();
const searchParams = new URLSearchParams();
searchParams.set('p', 'test');
render(<WrappedElement push={push} pathname='/hoge' searchParams={searchParams} />);
// 以下省略
});
});
なぜこれで動くの?
Next.jsのコードを読むと、それぞれコンテキストからこれらのデータを取ってきていることが分かります。
これら、使われているコンテキストのproviderでモックをセットしてあげることで、テスト実行時にモックが使われます。
参考
手前味噌ですが、こちらに書いた内容をちょっと見直したものです。