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
vitestfor testing.@testing-library/reactfor testing with React Query.mswfor mocking HTTP requests.jsdomfor mocking the DOM.@types/jsdomfor typings forjsdom@tanstack/react-queryfor 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());