Write fast unit and integration tests with Vitest — vitest.config.ts setup, vi.fn and vi.mock module mocking, fake timers, snapshots, V8 coverage with thresholds, workspaces for monorepos, and in-source testing.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Write fast unit and integration tests with Vitest — vitest.config.ts setup, vi.fn and vi.mock module mocking, fake timers, snapshots, V8 coverage with thresholds, workspaces for monorepos, and in-source testing.
This skill makes an AI agent write and configure Vitest test suites: a correct vitest.config.ts, module mocking with vi.mock and the vi.hoisted escape hatch, spies and fake timers, inline snapshots, V8 coverage gates, and projects config for monorepos. Trigger it on any Vite-based project, any repo with vitest in devDependencies, or when migrating from Jest.
Core Principles
Vitest reuses your Vite config — do not duplicate resolution logic. Aliases, plugins, and transforms from vite.config.ts apply to tests automatically. A separate Babel/transform setup is a Jest habit; drop it.
vi.mock is hoisted; factory variables are not. The mock factory runs before imports, so referencing top-level variables inside it throws. Use vi.hoisted() when the factory needs shared handles.
Prefer vi.fn injected via parameters over vi.mock of whole modules. Module mocking is a sledgehammer; dependency injection keeps tests honest and refactor-safe.
Inline snapshots over file snapshots for small values.toMatchInlineSnapshot puts the expectation in the test where reviewers see it; file snapshots get blindly --updated.
Coverage thresholds live in config and fail the run. A coverage report nobody gates on is wallpaper. Gate lines, functions, and branches — branch coverage is where the bugs hide.
Use the default node environment unless you render DOM.jsdom/happy-dom cost startup time per file; set them per-file with a docblock, not globally.
vitest run --project shared # one package
vitest run # everything, parallelized
In-source tests for small internal utilities (stripped from production builds by define: { 'import.meta.vitest': 'undefined' }):
// src/slug.tsexportfunctionslugify(input: string): string {
return input.toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
}
if (import.meta.vitest) {
const { expect, it } = import.meta.vitest;
it('collapses punctuation runs into single hyphens', () => {
expect(slugify(' Hello, World! ')).toBe('hello-world');
});
}
Best Practices
Set restoreMocks: true globally instead of sprinkling vi.restoreAllMocks() in every afterEach.
Use vitest related src/pricing.ts in pre-commit hooks to run only tests touching changed files.
Assert promise rejections with await expect(p).rejects.toThrow(...) — a bare expect(p).rejects without await can pass before settlement.
Pin the environment per file when only some tests need DOM: // @vitest-environment jsdom at the top of the file.
Prefer test.each for input tables over copy-pasted tests; each row reports as its own case.
When migrating from Jest: vi replaces jest, vi.mock factories must return the module shape explicitly (no automock), and jest.requireActual becomes importOriginal.
Anti-Patterns
Referencing top-level variables inside a vi.mock factory. Hoisting makes them undefined at factory time — the error message mentions hoisting, believe it. Use vi.hoisted.
globals: true plus missing TS types. If you enable globals, add "types": ["vitest/globals"] to tsconfig, or imports break silently in editors.
Giant .toMatchSnapshot() on full API responses. Hundred-line snapshots get rubber-stamp updated. Snapshot small, stable slices; assert dynamic fields with matchers.
vi.mock of the module under test. You end up testing your own mock. Mock dependencies, never the subject.
Forgetting vi.useRealTimers() cleanup — fake timers leak into later tests and hang anything that genuinely waits.
Re-implementing Vite aliases inside test.alias when they already exist in vite.config.ts; drift between the two breaks resolution in tests only.
When to Trigger This Skill
The project is Vite-based or has vitest in devDependencies.
The user asks to add unit tests, mock a module, fake timers, or snapshot output in a TS/JS repo without Jest.
Setting up coverage gates or monorepo test projects with package-specific environments.
Migrating a Jest suite to Vitest (jest → vi API mapping, mock factory differences).
Tests fail with hoisting errors, environment mismatches, or leaking mocks — the classic Vitest misconfigurations.