It still appears the next/router
is the suggested way by the official Next.js doc and we are continuing to use it in our projects. So the next/navigation
was not the solution for us.
What did work was creating a mock version of the router and injecting that mock via a context. Here is how:
// test-utils/mock-next-router.ts
import { NextRouter } from 'next/router';
export const mockNextRouter = (router: Partial<NextRouter>): NextRouter => ({
basePath: '',
pathname: '',
route: '',
query: {},
asPath: '/',
back: jest.fn(),
beforePopState: jest.fn(),
prefetch: jest.fn(),
push: jest.fn(),
reload: jest.fn(),
forward: jest.fn(),
replace: jest.fn(),
events: {
on: jest.fn(),
off: jest.fn(),
emit: jest.fn(),
},
isFallback: false,
isLocaleDomain: false,
isReady: true,
defaultLocale: 'en',
domainLocales: [],
isPreview: false,
...router,
});
// __tests__/fluffy-cats-card.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { describe, expect } from '@jest/globals';
import { RouterContext } from 'next/dist/shared/lib/router-context';
import { mockNextRouter } from '../test-utils/mock-next-router';
import FluffyCatsCard from '../components/fluffy-cats-card';
describe('Fluffy cats card component test', () => {
it('should call router.push when click on the "Click me!" button', async () => {
const router = mockNextRouter({});
render(
<RouterContext.Provider value={router}>
<FluffyCatsCard />
</RouterContext.Provider>
);
fireEvent.click(screen.getByRole('button', { name: 'Click me!' }));
expect(router.push).toHaveBeenCalledWith(
{ pathname: '/cats', query: { slug: 'fluffy' } },
undefined,
{});
});
});
// components/fluffy-cats-card.tsx
import React from "react";
import { useRouter } from "next/router";
import Image from "next/image";
import fluffyCatsImage from "../public/images/fluffy-cats.jpg";
export const FluffyCatsCard = () => {
const router = useRouter();
const showMeFluffyCats = () => {
// We don't use next/link for the sake of the example
router.push(
{ pathname: "/cats", query: { slug: "fluffy" } },
undefined,
{}
);
};
return (
<div className="card">
<Image src={fluffyCatsImage} alt="Fluffy cats" />
<button type="button" onClick={showMeFluffyCats}>
Click me!
</button>
</div>
);
};
export default FluffyCatsCard;
The original idea came from this GitHub repository https://github.com/bmvantunes/youtube-react-testing-video9-nextjs-router
It is a bit old but it worked for our case.
Note: Since the router accepts Partial<NextRouter>
you can customize the mock however you find fit. Ex. you may use mockNextRouter({ query: { id: 42 }})
to make sure your router will give you the id
parameter when you do router.query.id
in your component.