Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Next.js Testing

This is a simple example of how to test services with Vitest, MSW, React Testing Library on a Next.js application.

Requirements

Packages

  • vitest for testing.
  • @testing-library/react for testing with React Query.
  • msw for mocking HTTP requests.
  • jsdom for mocking the DOM.
  • @types/jsdom for typings for jsdom
  • @tanstack/react-query for state management.

How to write tests

Defining a query

We define a query in the queries.ts file located at the services directory of the feature. For example, to define a query to get a user by ID, we can add the following code:

// src/lib/features/feature1/services/queries.ts
import { useQuery } from "@tanstack/react-query";
import { fetchUser } from "./daos";

const userQueryKeys = {
  all: () => ["users"],
  byId: (id: string) => [...userQueryKeys.all(), id],
};

export const useGetUserByIdQuery = (id: string) => {
  return useQuery({
    queryKey: userQueryKeys.byId(id),
    queryFn: async () => await fetchUser(id),
  });
};

Mocking HTTP requests

We use Mock Service Worker to mock HTTP requests. This allows us to test the application without making real HTTP requests.

To mock a request, we need to define a route in the handlers.ts file located at the __tests__ directory of the server, more specifically in the list of handlers. For example, to mock a request to /users/:id, where :id can be anything, we can add the following code:

// src/lib/features/feature1/services/__tests__/handlers.ts
import { BASE_API_URL } from "@/lib/config/consts";
import { http, HttpResponse } from "msw";

export const handlers = [
  http.get(`${BASE_API_URL}/users/:id`, ({ params }) => {
    const { id } = params;
    if (id === "404") {
      return HttpResponse.json({ error: "User not found" }, { status: 404 });
    }
    return HttpResponse.json({ id, name: "User 1" });
  }),
];

Then, we can use the msw package to start the server and handle the requests.

import * as Feature1 from "@/lib/features/feature1/services/__tests__/handlers";
import { setupServer } from "msw/node";

export const server = setupServer(...Feature1.handlers);

For any other feature, add the handlers to the list of handlers. Let's say we have a second feature and we want to mock its handlers as well, we can add the following code:

import * as Feature1 from "@/lib/features/feature1/services/__tests__/handlers";
import * as Feature2 from "@/lib/features/feature2/services/__tests__/handlers";
import { setupServer } from "msw/node";

export const server = setupServer(...Feature1.handlers, ...Feature2.handlers);

Testing with React Query

We use the @tanstack/react-query package to manage state in our application. We can use the renderHook function from the @testing-library/react package to test the state of our application.

For example, to test the useGetUserByIdQuery hook, we can create a wrapper function that sets up the QueryClient and provides it to the hook.

// src/lib/features/feature1/services/__tests__/queries.test.tsx
import { renderHook, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useGetUserByIdQuery } from "../queries";
import { describe, test, expect } from "vitest";
import * as React from "react";

function createWrapper() {
  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        retry: false, // Disable retries for testing
      },
    },
  });
  return ({ children }: { children: React.ReactNode }) => (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
}

describe("useGetUserByIdQuery", () => {
  test("returns user when found", async () => {
    const { result } = renderHook(() => useGetUserByIdQuery("1"), {
      wrapper: createWrapper(),
    });

    await waitFor(() => expect(result.current.data).toBeDefined());
    expect(result.current.isSuccess).toBeTruthy();
    expect(result.current.data?.name).toBe("User 1");
  });

  test("handles user not found", async () => {
    const { result } = renderHook(() => useGetUserByIdQuery("404"), {
      wrapper: createWrapper(),
    });

    await waitFor(() => expect(result.current.isError).toBe(true));
    expect(result.current.error?.message).toBe("User not found");
  });
});

Testing with jsdom

We use the jsdom package to mock the DOM in our tests. This allows us to test the application without creating a DOM environment.

To use jsdom, we need to install the @types/jsdom package and add the following code to the vitest.config.ts file:

// vitest.config.ts
import { defineConfig } from "vitest/config";
import { resolve } from "path";

export default defineConfig({
  test: {
    environment: "jsdom",
    globals: true,
    setupFiles: ["./vitest.setup.ts"],
  },
  resolve: {
    alias: [{ find: "@", replacement: resolve(__dirname, "./src") }],
  },
});

And also setup the vitest.setup.ts file:

// vitest.setup.ts
import { beforeAll, afterEach, afterAll } from "vitest";
import { server } from "@/lib/config/tests/server";

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());