Vitest Setup
beginnerUnit and integration testing with Vitest and React Testing Library.
testingvitestunit-tests
Tested on⬢20▲16⚛19TS5.9
$ bunx sinew add testing/vitest-setupInteractive demo coming soon
1The Problem
Testing Next.js apps requires:
- Fast test execution
- TypeScript support without configuration
- Mocking of Next.js internals
- Component testing with React Testing Library
- Coverage reporting
2The Solution
Vitest provides a modern, fast testing experience with excellent TypeScript support.
3Files
vitest.config.ts
vitest.config.tsTypeScript
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"],
include: ["**/*.test.{ts,tsx}"],
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
exclude: ["node_modules/", "**/*.config.*", "**/*.d.ts"],
},
},
});vitest.setup.ts
vitest.setup.tsTypeScript
import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { afterEach, vi } from "vitest";
// Cleanup after each test
afterEach(() => {
cleanup();
});
// Mock Next.js router
vi.mock("next/navigation", () => ({
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
prefetch: vi.fn(),
back: vi.fn(),
}),
usePathname: () => "/",
useSearchParams: () => new URLSearchParams(),
}));
// Mock Next.js Image
vi.mock("next/image", () => ({
default: (props: React.ImgHTMLAttributes<HTMLImageElement>) => {
// eslint-disable-next-line @next/next/no-img-element
return <img {...props} alt={props.alt} />;
},
}));lib/test-utils.tsx
lib/test-utils.tsxTypeScript
import { render, RenderOptions } from "@testing-library/react";
import { ReactElement, ReactNode } from "react";
// Add providers here if needed
function AllProviders({ children }: { children: ReactNode }) {
return <>{children}</>;
}
function customRender(
ui: ReactElement,
options?: Omit<RenderOptions, "wrapper">
) {
return render(ui, { wrapper: AllProviders, ...options });
}
export * from "@testing-library/react";
export { customRender as render };components/__tests__/button.test.tsx
components/__tests__/button.test.tsxTypeScript
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@/lib/test-utils";
import userEvent from "@testing-library/user-event";
import { Button } from "../button";
describe("Button", () => {
it("renders with text", () => {
render(<Button>Click me</Button>);
expect(screen.getByRole("button")).toHaveTextContent("Click me");
});
it("calls onClick when clicked", async () => {
const user = userEvent.setup();
const onClick = vi.fn();
render(<Button onClick={onClick}>Click me</Button>);
await user.click(screen.getByRole("button"));
expect(onClick).toHaveBeenCalledTimes(1);
});
it("is disabled when disabled prop is true", () => {
render(<Button disabled>Click me</Button>);
expect(screen.getByRole("button")).toBeDisabled();
});
});lib/__tests__/utils.test.ts
lib/__tests__/utils.test.tsTypeScript
import { describe, it, expect } from "vitest";
import { formatDate, slugify } from "../utils";
describe("formatDate", () => {
it("formats a date correctly", () => {
const date = new Date("2025-01-15");
expect(formatDate(date)).toBe("January 15, 2025");
});
});
describe("slugify", () => {
it("converts string to slug", () => {
expect(slugify("Hello World")).toBe("hello-world");
});
it("handles special characters", () => {
expect(slugify("Hello! World?")).toBe("hello-world");
});
});package.json
package.jsonJSON
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage"
}
}4Dependencies
$ bun add -D vitest @testing-library/react5Configuration
Environment Variables in Tests
// vitest.config.ts
export default defineConfig({
test: {
env: {
DATABASE_URL: "postgresql://test:test@localhost:5432/test",
NEXT_PUBLIC_APP_URL: "http://localhost:3000",
},
},
});TypeScript
Test Filtering
# Run specific test file
bun test button.test.tsx
# Run tests matching pattern
bun test --testNamePattern="formats date"
# Run tests in watch mode
bun test --watchBash
6Usage
Testing Async Code
import { describe, it, expect } from "vitest";
import { fetchUser } from "../api";
describe("fetchUser", () => {
it("returns user data", async () => {
const user = await fetchUser("123");
expect(user).toHaveProperty("id", "123");
expect(user).toHaveProperty("email");
});
});TypeScript
Mocking Modules
import { vi, describe, it, expect } from "vitest";
vi.mock("@/lib/db", () => ({
prisma: {
user: {
findUnique: vi.fn().mockResolvedValue({ id: "1", name: "Test" }),
},
},
}));
describe("getUser", () => {
it("returns user from database", async () => {
const user = await getUser("1");
expect(user).toEqual({ id: "1", name: "Test" });
});
});TypeScript
Testing Hooks
import { renderHook, act } from "@testing-library/react";
import { useCounter } from "../use-counter";
describe("useCounter", () => {
it("increments count", () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
});TypeScript
7Troubleshooting
Tests running slowly
- Use
--pool=forksfor CPU-intensive tests - Run tests in parallel with
--pool=threads - Skip slow tests in watch mode with
.skip
Module resolution errors
- Ensure
tsconfigPathsplugin is configured - Check that path aliases match
tsconfig.json - Clear Vitest cache:
bun test --clearCache