MENU
Vitest
Vitest is a test runner built on top of Vite. It targets the same job as Jest -- running unit and component tests in a simulated environment -- but is built around native ES Modules and Vite's transform pipeline instead of Jest's own transformer and module system. Next.js officially supports Vitest as an alternative to Jest, with a maintained with-vitest example in the Next.js repository that scaffolds a working setup.
Why Teams Choose Vitest
- Speed: Vitest reuses Vite's dev-server transform pipeline and runs tests in worker threads with native ESM, which is typically noticeably faster than Jest's CommonJS-oriented transform-and-require pipeline, especially on watch mode re-runs where only changed modules get retransformed.
- Native ESM and the Vite ecosystem: If your project already uses Vite for anything, or leans heavily on ES Modules, Vitest fits without the extra interop Jest sometimes needs for ESM-only packages.
- Jest-compatible API: Vitest deliberately mirrors Jest's API -- describe, it, expect, mocking functions -- so a Jest test suite can often be ported with mostly import-path changes.
- Built-in TypeScript and JSX support via Vite's transform, without a separate SWC or Babel configuration step.
Jest remains the more established choice with the larger ecosystem of plugins and Stack Overflow answers; Vitest is generally the pick when raw test speed and ESM-native tooling matter more than ecosystem maturity. Either is a fully supported, valid choice for a Next.js project -- see Jest for the alternative setup.
npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/jest-domConfiguration
Where Jest uses the next/jest helper to bridge Next.js's SWC transform into Jest's config, Vitest instead uses the standard Vite React plugin directly -- there's no Next.js-specific wrapper needed, since Vitest already shares Vite's transform pipeline. The main config differences from a jest.config.js are the plugins array (Vite's plugin system instead of Jest transforms) and the test block living inside the same Vite config shape.
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',
setupFiles: ['./vitest.setup.ts'],
globals: true,
},
resolve: {
alias: {
'@': path.resolve(__dirname, './'),
},
},
})vitest.setup.ts:
import '@testing-library/jest-dom'Add a test script -- the CLI itself is deliberately Jest-like:
package.json:
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest"
}
}A Component Test, Ported from Jest
This is the same Counter component from the Jest page, tested with Vitest instead. Notice the test body itself is nearly identical -- render, screen, and userEvent all come from the same @testing-library/* packages regardless of which runner executes them. The only Vitest-specific line is the vi import used for spies/mocks, which plays the same role as Jest's global jest object.
components,Counter.test.tsx:
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import Counter from './Counter'
describe('Counter', () => {
it('starts at zero and increments on click', async () => {
const user = userEvent.setup()
render(<Counter />)
expect(screen.getByText('Count: 0')).toBeInTheDocument()
await user.click(screen.getByRole('button', { name: /increment/i }))
expect(screen.getByText('Count: 1')).toBeInTheDocument()
})
})Same Limitations as Jest
Vitest runs in Node.js against jsdom, just like Jest -- swapping the runner doesn't change what layer of the app it can reach. Async Server Components, Server Actions exercised through a real request, and anything else that depends on a genuinely running Next.js server are still outside what Vitest and React Testing Library can test directly. Use Playwright for that layer regardless of which unit test runner you choose.