Vitest vs Jest — Runners and Setup
TL;DR
Vitest is the modern default for Vite-based projects — same config, same transformers, fast, ESM-native, Jest-compatible API. Jest is the incumbent — broader plugin ecosystem, what existing codebases use. New projects on Vite → Vitest. Existing projects on Jest with no pain points → no need to migrate. The actual writing of tests is nearly identical; the differences are in startup, ESM/TS handling, and configuration ergonomics.
In depth
Why does Vitest exist?
Jest pre-dates ESM. Its transformer (Babel) re-parses your code through its own pipeline, separate from your app’s Vite/esbuild build. Result: slow startup, ESM friction, two configs to maintain.
Vitest reuses your app’s Vite config — same TS handling, same path aliases, same plugins. Tests run on the same toolchain as the app. Cold starts in hundreds of milliseconds; watch mode is near-instant.
API differences?
Almost none. Vitest’s API is Jest-compatible:
// Identical in both
import { describe, it, expect, beforeEach, vi /* or jest */ } from "vitest";
describe("counter", () => {
beforeEach(() => { /* setup */ });
it("starts at 0", () => {
expect(counter()).toBe(0);
});
});Differences:
viinstead ofjestfor the mocking API (vi.fn(),vi.mock(),vi.spyOn()).vitest.config.tsinstead ofjest.config.js.- A few less-common matchers differ in naming.
A Jest test file usually runs unchanged in Vitest with globals: true enabled in config (which gives you describe/it/expect as globals like Jest).
Minimal Vitest setup for a Vite + React + TS project?
Already have vite.config.ts. Add Vitest to it:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
// or "happy-dom" (faster)
environment: "jsdom",
setupFiles: "./test/setup.ts",
// enables describe/it/expect as globals
globals: true,
coverage: {
provider: "v8",
reporter: ["text", "html", "lcov"],
},
},
});// test/setup.ts
// matchers like toBeInTheDocument
import "@testing-library/jest-dom/vitest";
import { afterEach } from "vitest";
import { cleanup } from "@testing-library/react";
// tear down DOM between tests
afterEach(() => cleanup());// package.json
{
"scripts": {
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest --coverage"
}
}That’s it. vitest (interactive watch) and vitest run (one-shot for CI) are your two main commands.
jsdom vs happy-dom?
Both are JS-based DOM implementations. happy-dom is faster (~2-3×) but younger and slightly less complete. jsdom is the de-facto standard, slower but battle-tested.
| jsdom | happy-dom | |
|---|---|---|
| Speed | baseline | ~2-3x faster |
| Maturity | high | medium |
| Spec compliance | high | high but lags newer features |
| Default in | most setups | Vitest’s recommendation for new projects |
Start with happy-dom; switch to jsdom if you hit a missing feature. Both miss things — see “stubbing browser APIs” below.
How do you stub missing browser APIs?
Both jsdom and happy-dom lack IntersectionObserver, ResizeObserver, matchMedia, and a few others. Add stubs in setup:
// test/setup.ts
import { vi } from "vitest";
global.IntersectionObserver = vi.fn().mockImplementation(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
})),
});For more specialized cases, use libraries like @testing-library/jest-dom, jest-canvas-mock, etc.
Mocking — vi.mock and vi.fn?
// Mock a module
import { fetchUser } from "@/api/users";
vi.mock("@/api/users");
beforeEach(() => {
vi.mocked(fetchUser).mockResolvedValue({ id: 1, name: "Ada" });
});
// Spy without replacing
const spy = vi.spyOn(console, "log").mockImplementation(() => {});
// ...
spy.mockRestore();
// Stand-alone fn
const callback = vi.fn();
callback("hello");
expect(callback).toHaveBeenCalledWith("hello");vi.mock("module") is hoisted to the top of the file (like Jest’s jest.mock). The mock applies to all imports of the module in this test file.
Don’t vi.mock your own data-fetching layer if you can use MSW — see MSW — Network Mocking the Right Way. Mocking modules ties tests to implementation.
vi.useFakeTimers() — what’s it for?
Control time deterministically:
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it("debounces", () => {
const fn = vi.fn();
const debounced = debounce(fn, 300);
debounced();
expect(fn).not.toHaveBeenCalled();
vi.advanceTimersByTime(300);
expect(fn).toHaveBeenCalledOnce();
});Fake timers replace setTimeout, setInterval, setImmediate, Date.now(), process.nextTick. vi.advanceTimersByTime(ms) fires any pending timers within that window synchronously.
For Promise-based async, also await vi.runAllTimersAsync() — flushes microtasks too.
Snapshot testing in Vitest?
Same as Jest:
expect(component).toMatchSnapshot(); // file-based
expect(component).toMatchInlineSnapshot(); // inline in the test fileInline snapshots are easier to review in PRs. See Snapshot Testing — When and How (Sparingly) for when to use snapshots (sparingly).
Coverage in Vitest?
Configured in vite.config.ts test.coverage:
coverage: {
provider: "v8", // or "istanbul"
reporter: ["text", "html", "lcov"],
exclude: ["node_modules/", "test/", "**/*.d.ts"],
thresholds: {
lines: 80,
functions: 80,
branches: 75,
statements: 80,
},
}v8 is faster but less accurate around source maps; istanbul is the traditional choice. Gate CI on diff coverage (diff-cover) rather than total — see strategy file.
Running specific tests?
vitest # watch mode
vitest run # single run (CI)
vitest run path/to/file.test.ts # specific file
vitest run --reporter=verbose # detailed output
vitest --ui # interactive UI
vitest run -t "my test name" # by test name substring.only and .skip work as expected. .todo lets you stub tests you plan to write.
Migrating from Jest — how painful?
Usually a day or two for a medium codebase:
- Install
vitest,@vitest/coverage-v8,jsdom(orhappy-dom). - Add
testblock tovite.config.ts. - Replace
jest.config.jscontent with Vitest equivalents. - Find/replace
jest.fn()→vi.fn(),jest.mock→vi.mock, etc. (Vitest providesglobalsAliasfor compatibility, but explicitviis cleaner.) - Fix anything that depended on Jest-specific runtime quirks.
ESM-native projects often gain tests that couldn’t run under Jest’s CJS transformer. CJS-heavy projects with deep jest.mock reliance may need a migration plan rather than a flip.
Gotchas / edge cases
vi.mockis hoisted — calls to it run before imports. Side effects in mocks fire early.vi.useFakeTimers()doesn’t affect Promise microtasks by default — usevi.runAllTimersAsync()to flush microtasks.@testing-library/jest-dommatchers requireimport "@testing-library/jest-dom/vitest"in setup (the/vitestsubpath).global.fetchisn’t polyfilled by default; Node 18+ has it natively. For older or test isolation, MSW or undici.- Module-level constants captured at import time don’t see
vi.useFakeTimers()— fakes apply to subsequent calls, not the module’s initial evaluation. @vitest/coverage-v8doesn’t always show 100% onif/elsebranches that compile to short forms — Istanbul is more granular if you need it.
What a senior is expected to say 5
- “Vitest for Vite/ESM projects — reuses your app’s config, fast cold start, Jest-compatible API. Jest still fine for established projects with no pain.”
- “happy-dom by default for speed; jsdom if happy-dom lacks something you need. Stub
IntersectionObserver/ResizeObserver/matchMediain setup — neither implements them.” - “Mock at the network with MSW, not at modules with
vi.mock. Module mocks tie tests to implementation.” - “
vi.useFakeTimers()for time-dependent logic; pair withrunAllTimersAsyncfor Promise-based code.” - “Coverage is configured per-project; gate CI on diff coverage instead of total.”
Cross-references
- React Testing Library: React Testing Library — Query Priority, Role-Based Queries
- MSW (network mocking): MSW — Network Mocking the Right Way
- Async UI testing: Testing Async UI — waitFor, findBy*, Races
- Vue testing (uses Vitest too): Testing — Vue Test Utils + Vitest
Further reading
- Vitest docs: https://vitest.dev/
- Vitest API reference: https://vitest.dev/api/
- Migrating from Jest: https://vitest.dev/guide/migration.html#jest
- Testing Library + Vitest: https://testing-library.com/docs/dom-testing-library/setup