Vitest is the modern test runner for Next.js — Jest-compatible API, fast (Vite-powered), and works out of the box with TypeScript and React.
1npm i -D vitest @testing-library/react @testing-library/jest-dom @vitejs/plugin-react jsdom1import { defineConfig } from "vitest/config";
2import react from "@vitejs/plugin-react";
3import path from "node:path";
4
5export default defineConfig({
6 plugins: [react()],
7 test: {
8 environment: "jsdom",
9 globals: true,
10 setupFiles: ["./vitest.setup.ts"],
11 },
12 resolve: {
13 alias: { "@": path.resolve(__dirname, "./") },
14 },
15});1// vitest.setup.ts
2import "@testing-library/jest-dom/vitest";1// components/CourseCard.test.tsx
2import { render, screen } from "@testing-library/react";
3import { CourseCard } from "./CourseCard";
4
5const course = { id: "1", title: "Next.js 16", slug: "next-16", price: 2999 };
6
7test("renders title and price", () => {
8 render(<CourseCard course={course} />);
9 expect(screen.getByRole("heading", { name: /next\.js 16/i })).toBeInTheDocument();
10 expect(screen.getByText("₹2,999")).toBeInTheDocument();
11});Server Actions are just async functions. Test them like any other:
1// app/courses/actions.test.ts
2import { createCourse } from "./actions";
3import { db } from "@/lib/db";
4
5vi.mock("@/lib/db", () => ({
6 db: { course: { create: vi.fn() } },
7}));
8
9vi.mock("@/lib/auth-guard", () => ({
10 requireUser: vi.fn().mockResolvedValue({ id: "u1", role: "USER" }),
11}));
12
13test("creates a course with valid input", async () => {
14 (db.course.create as any).mockResolvedValue({ id: "c1", slug: "abc" });
15 const fd = new FormData();
16 fd.set("title", "My course");
17 fd.set("description", "A long enough description for validation.");
18 const result = await createCourse(null, fd);
19 expect(db.course.create).toHaveBeenCalled();
20});Server Components are async functions returning JSX. Test them directly:
1import Page from "./page";
2
3test("renders 5 courses", async () => {
4 const el = await Page({ params: Promise.resolve({}) });
5 render(el);
6 expect(screen.getAllByRole("article")).toHaveLength(5);
7});can.editCourse, role gatesWhat not to test in unit tests:
1{
2 "scripts": {
3 "test": "vitest",
4 "test:run": "vitest run",
5 "test:coverage": "vitest run --coverage"
6 }
7}Add to your CI: npm run test:run.