Vitest advertises a Jest-compatible API, and it delivers. describe, test, expect, toHaveBeenCalledWith — all identical. Which is why the five things that aren't compatible are so disorienting: the suite mostly runs, and then a handful of files fail for reasons the error messages don't explain.
1. Globals are opt-in
Jest injects describe and expect into the global scope. Vitest doesn't, by default.
// vitest.config.ts
export default defineConfig({
test: { globals: true },
})
Plus, for types:
// tsconfig.json
{ "compilerOptions": { "types": ["vitest/globals"] } }
The alternative — importing explicitly — is the better long-term choice, but do it as a separate commit. Mixing "migrate the runner" with "touch every test file" makes the diff unreviewable.
import { describe, test, expect, vi } from 'vitest'
2. moduleNameMapper doesn't exist
Path aliases move out of the test config and into Vite's resolver, where they're shared with your build:
// jest.config.js
moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1' }
// vitest.config.ts
import { fileURLToPath } from 'node:url'
export default defineConfig({
resolve: {
alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },
},
})
This is a net win — one place instead of two — but it means your test aliases now affect your bundle. Check them.
3. jest.mock hoists differently than you remember
Both hoist. The difference is what happens to variables the factory closes over. In Vitest you need vi.hoisted:
const { mockFetch } = vi.hoisted(() => ({ mockFetch: vi.fn() }))
vi.mock('./api', () => ({ fetchUser: mockFetch }))
Jest's jest.mock tolerated a variable named with a mock prefix as a special case. Vitest has no such carve-out, so the mockFoo naming convention that used to be load-bearing in your Jest suite now does nothing.
4. restoreMocks behaves differently than clearMocks
Three settings, easily confused, and the Jest defaults don't carry over:
| Option | Effect |
|---|---|
clearMocks |
Resets calls and instances between tests |
mockReset |
The above, plus resets the implementation to undefined
|
restoreMocks |
The above, plus restores the original implementation for spies |
If you relied on Jest's restoreMocks: true in jest.config.js, set it explicitly:
export default defineConfig({
test: { restoreMocks: true },
})
Symptom when you forget: a spy created with vi.spyOn in one test is still installed in the next one, and a completely unrelated test starts asserting against a stub.
5. Environments are per-file, and jsdom is not bundled
Vitest ships no DOM implementation. Install one:
npm i -D jsdom
Then set it globally, or per file — which is the feature Jest never had:
// @vitest-environment jsdom
That comment at the top of a file overrides the config for that file alone. Mixing node-environment and jsdom tests in one run stops being a config negotiation.
What to do first
Run the suite before changing a single test file. The failures cluster: aliases first, then mocks, then environment. Fixing them in that order means each batch of failures has one cause, rather than three tangled together.
The parts that don't break — assertions, matchers, snapshots, coverage output — are genuinely the bulk of a suite. The migration is real, but it's an afternoon, not a rewrite.
These posts come out of material I build for my Udemy courses — 25 of them now, mostly drill-based, across Go, Python, TypeScript, testing and Three.js. If this was useful, the full list is at udemy-c1f90.web.app. The links on that page carry a coupon I refresh each month, which usually lands around half the list price.
Top comments (0)