seed-skills/vitest-testing/SKILL.md
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.
npx skillsauth add PramodDutta/qaskills Vitest TestingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
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.vi.fn injected via parameters over vi.mock of whole modules. Module mocking is a sledgehammer; dependency injection keeps tests honest and refactor-safe.toMatchInlineSnapshot puts the expectation in the test where reviewers see it; file snapshots get blindly --updated.lines, functions, and branches — branch coverage is where the bugs hide.node environment unless you render DOM. jsdom/happy-dom cost startup time per file; set them per-file with a docblock, not globally.npm install --save-dev vitest @vitest/coverage-v8
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: false, // explicit imports; keeps files greppable and TS-clean
environment: 'node',
include: ['src/**/*.test.ts'],
setupFiles: ['./test/setup.ts'],
restoreMocks: true, // undo spy implementations between tests
coverage: {
provider: 'v8',
reporter: ['text', 'lcov', 'html'],
include: ['src/**'],
exclude: ['src/**/*.test.ts', 'src/types/**', 'src/main.ts'],
thresholds: {
lines: 85,
functions: 85,
branches: 75,
statements: 85,
},
},
},
});
// package.json scripts
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage"
}
}
Watch mode is the default vitest command and only reruns tests affected by the changed module graph — keep it running while developing.
// src/notifier.ts
export type SendEmail = (to: string, subject: string) => Promise<void>;
export async function notifyOnFailure(
jobName: string,
failures: number,
sendEmail: SendEmail,
): Promise<boolean> {
if (failures === 0) return false;
await sendEmail('[email protected]', `${jobName} failed ${failures} times`);
return true;
}
// src/notifier.test.ts
import { describe, expect, it, vi } from 'vitest';
import { notifyOnFailure } from './notifier';
describe('notifyOnFailure', () => {
it('emails oncall with the failure count in the subject', async () => {
const sendEmail = vi.fn().mockResolvedValue(undefined);
const sent = await notifyOnFailure('nightly-sync', 3, sendEmail);
expect(sent).toBe(true);
expect(sendEmail).toHaveBeenCalledExactlyOnceWith(
'[email protected]',
'nightly-sync failed 3 times',
);
});
it('stays silent when there are no failures', async () => {
const sendEmail = vi.fn();
await expect(notifyOnFailure('nightly-sync', 0, sendEmail)).resolves.toBe(false);
expect(sendEmail).not.toHaveBeenCalled();
});
});
import { beforeEach, expect, it, vi } from 'vitest';
import { getInvoice } from './invoice-service';
// Factory is hoisted above imports — capture handles via vi.hoisted
const { fetchMock } = vi.hoisted(() => ({ fetchMock: vi.fn() }));
vi.mock('./billing-client', () => ({
fetchInvoice: fetchMock,
}));
beforeEach(() => {
fetchMock.mockReset();
});
it('retries once on a 503 from the billing client', async () => {
fetchMock
.mockRejectedValueOnce(new Error('503 Service Unavailable'))
.mockResolvedValueOnce({ id: 'inv_42', total: 1999 });
const invoice = await getInvoice('inv_42');
expect(invoice.total).toBe(1999);
expect(fetchMock).toHaveBeenCalledTimes(2);
});
Partial mocks keep the rest of a module real:
vi.mock('./config', async (importOriginal) => {
const actual = await importOriginal<typeof import('./config')>();
return { ...actual, isFeatureEnabled: vi.fn().mockReturnValue(true) };
});
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
import { debounce } from './debounce';
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it('fires once after the trailing edge of 300ms', () => {
const fn = vi.fn();
const debounced = debounce(fn, 300);
debounced();
debounced();
vi.advanceTimersByTime(299);
expect(fn).not.toHaveBeenCalled();
vi.advanceTimersByTime(1);
expect(fn).toHaveBeenCalledTimes(1);
});
import { expect, it } from 'vitest';
import { formatReport, parseDuration } from './report';
it('formats a compact summary line', () => {
expect(formatReport({ passed: 12, failed: 1, skipped: 2 })).toMatchInlineSnapshot(
`"12 passed | 1 failed | 2 skipped"`,
);
});
it('throws a typed error on malformed durations', () => {
expect(() => parseDuration('5parsecs')).toThrowErrorMatchingInlineSnapshot(
`[RangeError: unknown duration unit "parsecs"]`,
);
});
// vitest.config.ts at the monorepo root
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
projects: [
{ test: { name: 'shared', root: './packages/shared', environment: 'node' } },
{ test: { name: 'web', root: './packages/web', environment: 'jsdom' } },
],
},
});
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.ts
export function slugify(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');
});
}
restoreMocks: true globally instead of sprinkling vi.restoreAllMocks() in every afterEach.vitest related src/pricing.ts in pre-commit hooks to run only tests touching changed files.await expect(p).rejects.toThrow(...) — a bare expect(p).rejects without await can pass before settlement.// @vitest-environment jsdom at the top of the file.test.each for input tables over copy-pasted tests; each row reports as its own case.vi replaces jest, vi.mock factories must return the module shape explicitly (no automock), and jest.requireActual becomes importOriginal.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..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.vi.useRealTimers() cleanup — fake timers leak into later tests and hang anything that genuinely waits.test.alias when they already exist in vite.config.ts; drift between the two breaks resolution in tests only.vitest in devDependencies.development
Build WebdriverIO E2E suites — wdio.conf.ts setup, $ and $$ selectors, auto-wait and waitUntil, Mocha framework structure, page objects, parallel capabilities, and services for visual testing and Appium mobile.
testing
Test Vue 3 components with Vue Test Utils and Vitest — mount vs shallowMount, finding and triggering DOM, asserting props and emitted events, awaiting async updates, and mocking Pinia stores and Vue Router.
development
Practice strict red-green-refactor test-driven development — write one failing test first, make it pass with the minimum code, then refactor under green, with worked cycles in Jest and pytest, AAA structure, and behavior-based test naming.
development
Test Node.js HTTP APIs in-process with SuperTest — request(app) without binding a port, chained .expect assertions, auth headers, JSON body validation, and Jest integration with proper async/await patterns.