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.
Interview Q&A
Q: Why does Vitest exist?
A: 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.
Q: API differences?
A: 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).
Q: Minimal Vitest setup for a Vite + React + TS project?
A: 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: {
environment: "jsdom", // or "happy-dom" (faster)
setupFiles: "./test/setup.ts",
globals: true, // enables describe/it/expect as globals
coverage: {
provider: "v8",
reporter: ["text", "html", "lcov"],
},
},
});
// test/setup.ts
import "@testing-library/jest-dom/vitest"; // matchers like toBeInTheDocument
import { afterEach } from "vitest";
import { cleanup } from "@testing-library/react";
afterEach(() => cleanup()); // tear down DOM between tests
// 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.
Q: jsdom vs happy-dom?
A: 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.
Q: How do you stub missing browser APIs?
A: 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.
Q: Mocking — vi.mock and vi.fn?
A:
// 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 05_msw_network_mocking.md. Mocking modules ties tests to implementation.
Q: vi.useFakeTimers() — what’s it for?
A: 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.
Q: Snapshot testing in Vitest?
A: Same as Jest:
expect(component).toMatchSnapshot(); // file-based
expect(component).toMatchInlineSnapshot(); // inline in the test file
Inline snapshots are easier to review in PRs. See 08_snapshot_testing.md for when to use snapshots (sparingly).
Q: Coverage in Vitest?
A: 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.
Q: Running specific tests?
A:
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.
Q: Migrating from Jest — how painful?
A: 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
- “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: 03_react_testing_library.md
- MSW (network mocking): 05_msw_network_mocking.md
- Async UI testing: 06_async_ui_testing.md
- Vue testing (uses Vitest too): ../06_vue/14_testing.md
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