Vitest 4 Testing Skill
Write, configure, and debug Vitest 4 test suites with Vite-native patterns.
Before You Start
This skill prevents 7+ common Vitest 4 mistakes and saves ~50% tokens.
| Metric | Without Skill | With Skill |
|---|
| Setup Time | ~90 min | ~30 min |
| Common Errors | 7+ | 0 |
| Token Usage | High (trial/error) | Low (known patterns) |
Known Issues This Skill Prevents
- Hanging agent runs from using watch mode instead of
- Broken coverage configs from using removed or
- Browser Mode spying failures from sealed ESM namespace objects
- Mock leakage between tests from missing restore/reset config
- Invalid multi-project setup from using deprecated terminology
- Wrong APIs from mixing Jest helpers into Vitest tests
- Flaky browser interactions from using synthetic helpers instead of
- Slow or unstable large suites from choosing the wrong execution pool or isolation mode
Quick Start
Step 1: Configure Vitest 4 for agent-safe runs
typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
restoreMocks: true,
clearMocks: true,
coverage: {
provider: 'v8',
include: ['src/**/*.{ts,tsx}'],
},
},
});
Why this matters: Vitest 4 removed
and
, and agent/CI environments need one-shot execution plus automatic mock cleanup.
Step 2: Write tests with Vitest APIs, not Jest APIs
typescript
import { describe, expect, it, vi } from 'vitest';
import { addUser } from './add-user';
import * as api from './api';
describe('addUser', () => {
it('returns the created user', async () => {
vi.spyOn(api, 'createUser').mockResolvedValue({ id: '1', name: 'Ada' });
await expect(addUser('Ada')).resolves.toEqual({ id: '1', name: 'Ada' });
});
});
Why this matters: is the supported mocking API. Mixing
or Jest-only patterns causes confusing failures and poor autocomplete.
Import rule: Import
,
,
, and
from
unless the project explicitly enables
.
Step 3: Use the correct runtime command
bash
vitest run
vitest run --coverage
vitest run path/to/example.test.ts
Why this matters: without
starts watch mode by default in development, which is a poor fit for agents, CI, and non-interactive verification.
Critical Rules
Always Do
- Use or for agent and CI workflows
- Prefer
vi.mock(import('./module'))
for type-safe module mocks
- Configure , , or intentionally
- Use for multi-project configs; the rename began in Vitest 3.2 and older workspace-file usage is removed in Vitest 4
- Use a shared base config when multiple need common settings; projects do not inherit root config unless you opt in
- Use to report on untested source files
- Use and from in Browser Mode
- Prefer when native modules or runtime compatibility matter more than raw speed
- Share and the implementation file when asking AI to generate tests
Never Do
- Never use , , or Jest-only globals in Vitest code
- Never rely on removed or in Vitest 4
- Never use plain watch mode for agent-driven verification
- Never use on native ESM exports in Browser Mode
- Never forget that is hoisted before the rest of the file executes
- Never leave env/global stubs un-restored across tests
Common Mistakes
Wrong - removed coverage option:
typescript
export default defineConfig({
test: {
coverage: {
all: true,
},
},
});
Correct - use include globs:
typescript
export default defineConfig({
test: {
coverage: {
provider: 'v8',
include: ['src/**/*.{ts,tsx}'],
},
},
});
Why: Vitest 4 removed
and
;
is the supported way to include uncovered files.
Wrong - Browser Mode spy on sealed export:
typescript
import * as math from './math';
import { vi } from 'vitest';
vi.spyOn(math, 'add').mockReturnValue(10);
Correct - use spy-enabled module mock:
typescript
import { vi } from 'vitest';
vi.mock(import('./math'), { spy: true });
Why: Native browser ESM namespace objects are sealed, so direct spies on exports fail in Browser Mode.
Known Issues Prevention
| Issue | Root Cause | Solution |
|---|
| Tests never exit | Watch mode started in a non-interactive session | Use |
| Coverage report misses untested files | not configured | Add explicit source globs |
| Browser Mode spy throws or does nothing | used on sealed ESM exports | Use vi.mock(import('./mod'), { spy: true })
|
| Mocks leak between tests | Cleanup flags missing | Enable / / |
| Multi-project config breaks after upgrade | Deprecated workspace terminology or removed workspace-file patterns carried over | Switch to and |
| Worker or pool config stops working | Old , , or carried forward | Migrate to Vitest 4 worker settings such as |
| Project-specific config unexpectedly disappears | Root config assumptions are not inherited into | Use , , or a shared base explicitly |
| AI-generated tests use wrong helpers | Jest patterns copied into Vitest | Replace with , Vitest imports, and Vitest matchers |
| Browser tests hang | Blocking dialogs or wrong user-event utilities | Mock dialogs and use helpers |
| Fast pool causes strange native-module failures | chosen for a suite that needs process isolation | Switch to or narrow thread usage |
Bundled Resources
References
- Mocking rules and hoisting →
references/mocking-reference.md
- Browser Mode providers and pitfalls →
references/browser-mode-reference.md
- Coverage and multi-project config →
references/coverage-projects-reference.md
- Pools, isolation, and persistent cache →
references/pools-execution-reference.md
- Reference index →
Configuration Reference
vitest.config.ts
typescript
import { defineConfig, defineProject } from 'vitest/config';
import { playwright } from '@vitest/browser-playwright';
export default defineConfig({
test: {
projects: [
defineProject({
test: {
name: 'unit',
include: ['src/**/*.test.ts'],
environment: 'node',
},
}),
defineProject({
test: {
name: 'browser',
include: ['src/**/*.browser.test.ts'],
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
},
}),
],
coverage: {
provider: 'v8',
include: ['src/**/*.{ts,tsx}'],
},
restoreMocks: true,
unstubEnvs: true,
setupFiles: ['./test/setup.ts'],
},
});
Key settings:
- : Stable multi-project terminology; the rename started in Vitest 3.2, and projects do not automatically inherit every root config value, so shared settings should be factored into a reused base when needed
- : Required when uncovered source files must appear in the report
- : In Vitest 4, import the provider factory from the provider package, such as
- / : Prevent test pollution across files
- : Run shared test initialization such as MSW, globals, or polyfills before test files
Project Structure
my-app/
├── src/
│ ├── feature.ts
│ ├── feature.test.ts
│ └── feature.browser.test.ts
├── vitest.config.ts
├── vite.config.ts
└── package.json
Why this matters: Keeping Node and Browser Mode tests clearly separated makes provider setup, test selection, and troubleshooting much simpler.
Choose the right environment: Prefer
for most component tests and lightweight DOM assertions. Use Browser Mode when native browser APIs, real layout/event behavior, or screenshot assertions matter.
Choose the right execution model: Prefer
for stability and native-module compatibility, especially in mixed or infrastructure-heavy suites. Reach for
only when you know the test environment is safe for worker-thread execution and the extra speed matters.
Common Patterns
Type-safe module mock pattern
typescript
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { getUserName } from './get-user-name';
import * as api from './api';
vi.mock(import('./api'), () => ({
fetchUser: vi.fn(),
}));
describe('getUserName', () => {
beforeEach(() => {
vi.mocked(api.fetchUser).mockReset();
});
it('returns the fetched user name', async () => {
vi.mocked(api.fetchUser).mockResolvedValue({ id: '1', name: 'Ada' });
await expect(getUserName('1')).resolves.toBe('Ada');
});
});
Browser Mode interaction pattern
typescript
import { expect, test } from 'vitest';
import { page, userEvent } from 'vitest/browser';
import { render } from 'vitest-browser-react';
import { Counter } from './counter';
test('increments after click', async () => {
render(<Counter />);
await userEvent.click(page.getByRole('button', { name: /increment/i }));
await expect.element(page.getByText('1')).toBeInTheDocument();
});
In-source testing pattern
typescript
export function sum(a: number, b: number) {
return a + b;
}
if (import.meta.vitest) {
const { it, expect } = import.meta.vitest;
it('adds numbers', () => {
expect(sum(1, 2)).toBe(3);
});
}
Dependencies
Required
| Package | Version | Purpose |
|---|
| ^4 | Test runner and assertion/mocking APIs |
| ^6 | Shared Vite-powered module pipeline |
| >=20 | Required runtime for Vitest 4 |
Optional
| Package | Version | Purpose |
|---|
| ^4 | Fast, accurate coverage with AST remapping |
@vitest/coverage-istanbul
| ^4 | Istanbul coverage backend |
@vitest/browser-playwright
| ^4 | Playwright provider for Browser Mode |
@vitest/browser-webdriverio
| ^4 | WebdriverIO provider for Browser Mode |
| ^4 | Preview provider for Browser Mode |
Official Documentation
Troubleshooting
Agent run hangs forever
Symptoms: The test process never exits or Claude waits for additional file changes.
Solution:
Browser Mode test cannot spy on export
Symptoms: throws, does nothing, or works in Node mode but fails in browser.
Solution:
typescript
vi.mock(import('./module'), { spy: true });
Coverage misses source files with no tests
Symptoms: The report only contains files touched by executed tests.
Solution:
typescript
coverage: {
provider: 'v8',
include: ['src/**/*.{ts,tsx}'],
}
Legacy worker or pool settings break after upgrade
Symptoms: Old
,
,
,
, or
settings stop working after moving to Vitest 4.
Solution:
Why: Vitest 4 simplified worker configuration and removed several older pool-specific options.
Setup Checklist
Before using this skill, verify: