Vitest Setup with Next.js
Configure Vitest as a fast, modern test runner for your Next.js App Router project.
Recipe
Quick-reference recipe card -- copy-paste ready.
# Install dependencies
npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./vitest.setup.ts"],
include: ["**/*.test.{ts,tsx}"],
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
});// vitest.setup.ts
import "@testing-library/jest-dom/vitest";// package.json scripts
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
}
}When to reach for this: When starting a new Next.js project and you want a fast, Vite-native test runner with near-instant HMR-based watch mode.
Working Example
// src/components/greeting.tsx
interface GreetingProps {
name: string;
}
export function Greeting({ name }: GreetingProps) {
return <h1>Hello, {name}!</h1>;
}// src/components/greeting.test.tsx
import { render, screen } from "@testing-library/react";
import { describe, it, expect } from "vitest";
import { Greeting } from "./greeting";
describe("Greeting", () => {
it("renders the name", () => {
render(<Greeting name="Alice" />);
expect(screen.getByRole("heading")).toHaveTextContent("Hello, Alice!");
});
it("updates when name prop changes", () => {
const { rerender } = render(<Greeting name="Alice" />);
expect(screen.getByRole("heading")).toHaveTextContent("Hello, Alice!");
rerender(<Greeting name="Bob" />);
expect(screen.getByRole("heading")).toHaveTextContent("Hello, Bob!");
});
});What this demonstrates:
- Minimal Vitest config with React plugin and jsdom
- Setup file that loads jest-dom matchers for Vitest
- Path alias matching
@/from tsconfig - A simple component test using Testing Library
Deep Dive
How It Works
- Vitest uses Vite's transform pipeline under the hood, so JSX/TSX files are compiled via
@vitejs/plugin-reactwithout needing a separate Babel config - The
environment: "jsdom"setting creates a simulated browser DOM in Node.js for each test file globals: truemakesdescribe,it,expectavailable without importing them (matching Jest conventions)- The setup file runs before every test file -- loading
@testing-library/jest-dom/vitestadds matchers liketoBeInTheDocument()andtoHaveTextContent() - Watch mode uses Vite's module graph to only re-run tests affected by changed files, making it much faster than Jest's watch mode
Variations
Using happy-dom instead of jsdom:
// vitest.config.ts
export default defineConfig({
test: {
environment: "happy-dom", // faster but less complete DOM implementation
},
});Per-file environment override:
// @vitest-environment happy-dom
import { describe, it } from "vitest";
// This file uses happy-dom regardless of global configCoverage with v8 provider:
npm install -D @vitest/coverage-v8// vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: "v8",
reporter: ["text", "html", "lcov"],
include: ["src/**/*.{ts,tsx}"],
exclude: ["src/**/*.test.{ts,tsx}", "src/**/*.d.ts"],
},
},
});App Router path aliases from tsconfig:
// vitest.config.ts — match tsconfig paths exactly
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [react(), tsconfigPaths()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./vitest.setup.ts"],
},
});npm install -D vite-tsconfig-pathsTypeScript Notes
// If using globals: true, add vitest types to tsconfig
// tsconfig.json
{
"compilerOptions": {
"types": ["vitest/globals", "@testing-library/jest-dom"]
}
}Gotchas
-
Missing
@vitejs/plugin-react-- Without this plugin, JSX in test files fails to transform. Vitest does not automatically handle JSX like Jest with Babel does. Fix: Always includereact()in the plugins array. -
Path aliases not resolving -- If you use
@/components/...imports, Vitest does not readtsconfig.jsonpaths by default. Fix: Either setaliasmanually invitest.config.tsor usevite-tsconfig-paths. -
globals: truebut TypeScript errors -- TypeScript does not know about Vitest globals unless you add"vitest/globals"to yourcompilerOptions.types. Fix: Updatetsconfig.jsonas shown above. -
jsdom vs happy-dom --
happy-domis faster but lacks some DOM APIs (getBoundingClientRect,IntersectionObserver). If tests fail withhappy-dom, switch tojsdom. -
Next.js server-only code -- Vitest cannot run code that uses
next/headers,next/cache, or other Node-only Next.js APIs in ajsdomenvironment. Fix: Mock those modules or test them separately.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Jest with next/jest | You have an existing Jest setup or need the broader Jest ecosystem | You want faster watch mode and native ESM support |
| Playwright Component Testing | You need real browser rendering for component tests | You want fast unit tests that run in Node |
| Bun test runner | Your project uses Bun as its runtime | You need the Vitest/Jest ecosystem of matchers and plugins |
FAQs
Why do I need @vitejs/plugin-react in Vitest config?
- Vitest does not transform JSX by default like Jest with Babel does.
- Without the
react()plugin, any JSX in test or source files will fail to compile. - Always include
react()in thepluginsarray ofvitest.config.ts.
What is the difference between jsdom and happy-dom environments?
jsdomis a more complete browser DOM implementation but slower.happy-domis faster but lacks some APIs likegetBoundingClientRectandIntersectionObserver.- If tests fail under
happy-dom, switch tojsdom.
How do I make describe, it, and expect available globally without imports?
Set globals: true in vitest.config.ts and add "vitest/globals" to your tsconfig.json compilerOptions.types array.
What does the vitest.setup.ts file do?
- It runs before every test file.
- Importing
@testing-library/jest-dom/vitestadds matchers liketoBeInTheDocument()andtoHaveTextContent().
How do I resolve @/ path aliases in Vitest?
Either set alias manually in vitest.config.ts:
alias: {
"@": path.resolve(__dirname, "./src"),
}Or install and use vite-tsconfig-paths as a plugin.
Gotcha: Why does TypeScript complain about expect and describe even though tests run fine?
globals: truemakes them available at runtime, but TypeScript does not know about them.- You must add
"vitest/globals"tocompilerOptions.typesintsconfig.json.
How do I set up code coverage with Vitest?
npm install -D @vitest/coverage-v8Then add coverage.provider: "v8" and your desired reporters in vitest.config.ts.
Can I override the test environment on a per-file basis?
Yes. Add a comment at the top of the test file:
// @vitest-environment happy-domThis overrides the global environment setting for that file only.
Gotcha: Why do my tests fail when importing next/headers or next/cache?
- These are Node-only Next.js APIs that do not work in a
jsdomenvironment. - You must mock those modules in your tests or test them in a separate environment.
How do I type the vitest.config.ts file correctly in TypeScript?
Import defineConfig from vitest/config:
import { defineConfig } from "vitest/config";
export default defineConfig({ /* ... */ });This gives you full type-checking and autocompletion for all Vitest config options.
What is the difference between vitest and vitest run?
viteststarts watch mode, re-running tests when files change.vitest runexecutes all tests once and exits -- use this in CI.
How does Vitest watch mode know which tests to re-run?
It uses Vite's module graph to track dependencies. Only tests affected by changed files are re-run, making it much faster than Jest's watch mode.
Related
- Jest Setup with Next.js -- the alternative test runner
- React Testing Library Fundamentals -- how to query and assert in tests
- Mocking in Tests -- mocking modules and APIs
- Testing Strategy & Best Practices -- choosing what to test