Vitest is the right unit testing choice for Next.js and Vite-based projects in 2026. It shares your project's Vite or SWC configuration (no separate Babel setup), runs tests significantly faster than Jest in most TypeScript projects, and its API is Jest-compatible so migration is straightforward. The one catch with Next.js is that you need the jsdom environment explicitly, since Next.js is not Vite-native.
What Vitest Is
Vitest is a test runner built by the Vite team. It uses Vite's module resolution, TypeScript transformation, and plugin system. For Vite-based projects, this means your test environment uses exactly the same configuration as your application. No separate Babel configuration, no ts-jest, no babel-jest. TypeScript just works.
The speed difference is real. Vitest uses Vite's fast ESM-based transformation pipeline. On a large TypeScript codebase, Vitest commonly runs 2-5x faster than Jest. The hot module replacement in watch mode makes the test loop feel instant.
Setting Up in a Next.js Project
Next.js uses SWC (not Vite) for its compilation, but Vitest can still be used for unit and component tests. You configure Vitest with the @vitejs/plugin-react plugin and the jsdom environment:
npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/user-event
Create vitest.config.ts:
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import { resolve } from "path";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./tests/setup.ts"],
},
resolve: {
alias: {
"@": resolve(__dirname, "./"),
},
},
});
Create tests/setup.ts:
import "@testing-library/jest-dom";
Add a test script to package.json:
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
}
}
Core API: describe, it, expect, vi
Vitest's API is intentionally Jest-compatible. If you know Jest, you know Vitest. The only difference is vi instead of jest for mocking utilities:
import { describe, it, expect, vi, beforeEach } from "vitest";
describe("formatCurrency", () => {
it("formats USD correctly", () => {
expect(formatCurrency(1234.56, "USD")).toBe("$1,234.56");
});
it("handles zero", () => {
expect(formatCurrency(0, "USD")).toBe("$0.00");
});
it("handles negative values", () => {
expect(formatCurrency(-100, "USD")).toBe("-$100.00");
});
});
Mocking Modules and Functions
Use vi.mock to mock entire modules and vi.fn to create mock functions:
import { describe, it, expect, vi, beforeEach } from "vitest";
import { sendEmail } from "@/lib/email";
import { createUser } from "@/lib/users";
// Mock the email module
vi.mock("@/lib/email", () => ({
sendEmail: vi.fn().mockResolvedValue({ id: "email-123" }),
}));
describe("createUser", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("sends a welcome email after creating a user", async () => {
await createUser({ email: "user@example.com", name: "Test User" });
expect(sendEmail).toHaveBeenCalledOnce();
expect(sendEmail).toHaveBeenCalledWith(
expect.objectContaining({
to: "user@example.com",
subject: expect.stringContaining("Welcome"),
})
);
});
});
Testing React Components with Testing Library
@testing-library/react renders components in a jsdom environment and provides queries for finding elements:
import { describe, it, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { LoginForm } from "@/components/auth/login-form";
describe("LoginForm", () => {
it("shows validation error for invalid email", async () => {
const user = userEvent.setup();
render(<LoginForm onSubmit={vi.fn()} />);
await user.type(screen.getByLabelText("Email"), "not-an-email");
await user.click(screen.getByRole("button", { name: "Sign In" }));
expect(screen.getByText("Please enter a valid email address")).toBeInTheDocument();
});
it("calls onSubmit with credentials when form is valid", async () => {
const user = userEvent.setup();
const onSubmit = vi.fn();
render(<LoginForm onSubmit={onSubmit} />);
await user.type(screen.getByLabelText("Email"), "user@example.com");
await user.type(screen.getByLabelText("Password"), "password123");
await user.click(screen.getByRole("button", { name: "Sign In" }));
expect(onSubmit).toHaveBeenCalledWith({
email: "user@example.com",
password: "password123",
});
});
});
The key philosophy of Testing Library: test what the user sees, not implementation details. Query elements by their accessible role, label, or text content. Avoid querying by CSS class or component internal state.
Setup Files and Shared Configuration
The setupFiles configuration in Vitest runs before each test file. Use it to configure global mocks that apply to every test:
// tests/setup.ts
import "@testing-library/jest-dom";
import { vi } from "vitest";
// Mock Next.js router for all tests
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
prefetch: vi.fn(),
back: vi.fn(),
}),
usePathname: () => "/",
useSearchParams: () => new URLSearchParams(),
}));
Vitest vs Jest: When to Use Each
Use Vitest for:
- Any project using Vite (React, Vue, Svelte apps built with Vite)
- Next.js projects where speed and zero-config TypeScript matter
- New projects starting fresh in 2026
- Teams coming from Jest who want a faster experience with minimal migration effort
Use Jest for:
- Legacy projects where the migration cost outweighs the speed gain
- Projects with heavy reliance on Jest-specific plugins that do not have Vitest equivalents
- Monorepos where some packages have non-Vite build tools and you want a unified test runner
In practice, most modern JavaScript projects benefit from switching to Vitest. The Jest compatibility means most test code can be migrated by find-replacing jest. with vi..
Coverage Reporting
npm run test:coverage
Vitest uses V8 coverage (built into Node.js) or Istanbul (@vitest/coverage-istanbul). V8 coverage has zero additional dependencies and works well for most cases. Configure coverage thresholds to enforce minimum coverage levels in CI:
test: {
coverage: {
provider: "v8",
reporter: ["text", "html", "lcov"],
thresholds: {
lines: 80,
functions: 80,
branches: 70,
},
},
},
Keep Reading
- Playwright Testing Guide — E2E tests that complement your Vitest unit tests
- CI/CD for Small Engineering Teams — running Vitest in GitHub Actions
- TypeScript for React Developers — typing your components and functions for better testability
Pristren builds AI-powered software for teams. Zlyqor is our all-in-one workspace — chat, projects, time tracking, AI meeting summaries, and invoicing — in one tool. Try it free.