Vitest Setup
Vitest is Jest-compatible but dramatically faster thanks to Vite's module graph. Set it up alongside Next.js with a simple vitest.config.ts that maps the same module aliases as your tsconfig.json.
Unit Testing with Vitest
Maintaining code quality as a product features list grows requires automated verification. Unit testing isolates individual software functions—such as date formatters, price calculators, or Server Actions—and tests their responses against set conditions. While Jest was the historical standard for Node.js, Vitest is the modern standard for Next.js 15. Vitest integrates directly with Vite-based build pipelines, parses TypeScript out of the box, and provides multi-threaded testing runs with fast hot-reload feedback. In this lesson, we will cover how to configure Vitest, write unit tests, mock external APIs, and validate Server Actions.
1. What is Vitest and Why Do We Prefer It?
Vitest is a Vite-native test runner. It provides a Jest-compatible assertion API (describe, test, expect, vi) but resolves file imports through Vite's module graph, making it significantly faster than Jest which relies on heavy Babel transpilation.
Primary Advantages in Next.js:
- Instant Hot Reload: Only re-runs tests that import files you modified.
- Path Mapping Support: Automatically imports path mappings from your
tsconfig.json. - Edge Compatibility Mocking: Exposes mock setups for Edge global APIs.
2. Installing Vitest and React Plugin Adapters
Install the test runner and react helper extensions:
npm install -D vitest @vitejs/plugin-react jsdom
Here:
vitestis the runner client.@vitejs/plugin-reactenables JSX parsing.jsdomprovides a browser-like DOM environment inside Node.js.
3. Configuring vitest.config.ts
Create a configuration file in your project root to handle loaders and path alias structures:
// 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', // Mock DOM environment for testing components
globals: true, // Auto-expose global APIs (describe, expect, etc.)
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'), // Maps import path aliases
},
},
});
4. Writing a Simple Unit Test
Let's write a unit test for a utility function that formats prices. Create a test file alongside your code named format.test.ts:
// utils/format.ts
export function formatCurrency(amount: number, currency = 'USD'): string {
if (isNaN(amount)) return '$0.00';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
}).format(amount);
}
Now write the test validations:
// utils/format.test.ts
import { describe, it, expect } from 'vitest';
import { formatCurrency } from './format';
describe('formatCurrency utility tests', () => {
it('formats positive numbers correctly', () => {
expect(formatCurrency(10.5)).toBe('$10.50');
});
it('handles zero values cleanly', () => {
expect(formatCurrency(0)).toBe('$0.00');
});
it('returns default fallback on NaN', () => {
expect(formatCurrency(NaN)).toBe('$0.00');
});
it('supports custom currencies', () => {
expect(formatCurrency(10, 'EUR')).toContain('10.00');
});
});
5. Mocking Third-Party Modules and APIs
Often, utility functions import database clients or network services (like Stripe or fetch requests). You must mock these dependencies inside your unit tests to isolate your function logic from external network checks.
// utils/api.ts
export async function fetchUserEmail(userId: string): Promise<string> {
const res = await fetch('https://api.progrumar.com/users/' + userId);
if (!res.ok) throw new Error('User not found');
const data = await res.json();
return data.email;
}
Here is how to mock the global fetch function inside Vitest:
// utils/api.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { fetchUserEmail } from './api';
describe('fetchUserEmail mock tests', () => {
beforeEach(() => {
vi.restoreAllMocks(); // Clear mock tracking states before each test run
});
it('resolves email on successful HTTP responses', async () => {
// Mock the global fetch function
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ email: 'test@progrumar.com' }),
});
vi.stubGlobal('fetch', mockFetch);
const email = await fetchUserEmail('123');
expect(email).toBe('test@progrumar.com');
expect(mockFetch).toHaveBeenCalledWith('https://api.progrumar.com/users/123');
});
it('throws error when server responds with 404', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: false,
});
vi.stubGlobal('fetch', mockFetch);
await expect(fetchUserEmail('404')).rejects.toThrow('User not found');
});
});
6. Testing Server Actions
Server Actions are simply async functions. You can test them like standard Javascript promises, mocking database calls to ensure they validate parameters:
// app/actions/user.ts
'use server';
import { db } from '@/db';
import { users } from '@/db/schema';
import { eq } from 'drizzle-orm';
export async function deleteUser(id: number) {
if (id <= 0) return { error: 'Invalid ID' };
await db.delete(users).where(eq(users.id, id));
return { success: true };
}
Mock the database client inside your test runner:
// app/actions/user.test.ts
import { describe, it, expect, vi } from 'vitest';
import { deleteUser } from './user';
// Mock database client module path
vi.mock('@/db', () => ({
db: {
delete: vi.fn().mockReturnThis(),
where: vi.fn().mockResolvedValue({}),
},
}));
describe('deleteUser Server Action tests', () => {
it('rejects execution on invalid user IDs', async () => {
const res = await deleteUser(-1);
expect(res.error).toBe('Invalid ID');
});
it('runs database delete query on valid IDs', async () => {
const res = await deleteUser(5);
expect(res.success).toBe(true);
});
});
7. CLI commands and Watch Mode
By default, running Vitest initializes watch mode, keeping the test runner running in your terminal to re-evaluate code changes on save:
npx vitest
To execute a one-time test check (useful in CI/CD pipeline deployments), use the run flag:
npx vitest run
8. Common Gotchas
-
Forgetting to Clear Stubbed Globals: Stubbing global variables (like
vi.stubGlobal('fetch', ...)) persists across tests. If you forget to callvi.restoreAllMocks()insidebeforeEach, other test suites might fail due to mock contamination. -
Mocking Node-specific APIs inside Browser Environments: If your config sets
environment: 'jsdom', calling specific Node APIs (like raw file systemsfs) inside unit tests might throw errors unless you mock them out.
Key Takeaways
- Vitest operates on Vite's module loader, making it significantly faster than Jest.
- Configure Vitest using
vitest.config.tsat the project root. - Mock API calls using
vi.stubGlobalor module level mocks (vi.mock). - Test Server Actions as standard async JS promises.
- Use
npx vitest runin deployment pipelines to run tests once.
Unit tests validate isolated logical functions, but applications also require user interface testing. In the next lesson, we will cover Component Testing with React Testing Library, learning how to mock React hooks and render components to test client-side clicks and inputs.