seed-skills/vitest/SKILL.md
Modern JavaScript and TypeScript testing with Vitest, covering unit testing, integration testing, mocking, snapshots, browser mode, and Vite integration.
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.
You are an expert software engineer specializing in testing with Vitest. When the user asks you to write, review, or debug Vitest tests, follow these detailed instructions.
project/
src/
components/
Button.tsx
Button.test.tsx
services/
user.service.ts
user.service.test.ts
utils/
validators.ts
validators.test.ts
tests/
integration/
api.test.ts
db.test.ts
fixtures/
test-data.ts
setup.ts
vitest.config.ts
vite.config.ts
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './tests/setup.ts',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'tests/',
'**/*.test.ts',
'**/*.spec.ts',
'**/types/',
],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
include: ['**/*.{test,spec}.{ts,tsx}'],
exclude: ['node_modules', 'dist', 'build'],
testTimeout: 10000,
hookTimeout: 10000,
},
});
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
environmentMatchGlobs: [
['**/*.test.tsx', 'jsdom'],
['**/*.browser.test.ts', 'jsdom'],
['**/*.node.test.ts', 'node'],
['**/*.edge.test.ts', 'edge-runtime'],
],
poolOptions: {
threads: {
singleThread: false,
isolate: true,
},
},
coverage: {
provider: 'v8',
include: ['src/**/*.{ts,tsx}'],
},
benchmark: {
include: ['**/*.bench.{ts,tsx}'],
},
},
});
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { sum, multiply } from './math';
describe('Math utilities', () => {
it('should add two numbers', () => {
expect(sum(2, 3)).toBe(5);
});
it('should multiply two numbers', () => {
expect(multiply(4, 5)).toBe(20);
});
it('should handle zero', () => {
expect(sum(0, 0)).toBe(0);
expect(multiply(5, 0)).toBe(0);
});
it('should handle negative numbers', () => {
expect(sum(-1, 1)).toBe(0);
expect(multiply(-2, 3)).toBe(-6);
});
});
// user.service.ts
export class UserService {
constructor(
private apiClient: ApiClient,
private cache: Cache
) {}
async getUser(id: string): Promise<User> {
const cached = await this.cache.get(`user:${id}`);
if (cached) return JSON.parse(cached);
const user = await this.apiClient.get(`/users/${id}`);
await this.cache.set(`user:${id}`, JSON.stringify(user), 3600);
return user;
}
async createUser(data: CreateUserDto): Promise<User> {
return this.apiClient.post('/users', data);
}
}
// user.service.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { UserService } from './user.service';
describe('UserService', () => {
let userService: UserService;
let mockApiClient: any;
let mockCache: any;
beforeEach(() => {
mockApiClient = {
get: vi.fn(),
post: vi.fn(),
};
mockCache = {
get: vi.fn(),
set: vi.fn(),
};
userService = new UserService(mockApiClient, mockCache);
});
describe('getUser', () => {
it('should return cached user if available', async () => {
const cachedUser = { id: '1', name: 'Cached User' };
mockCache.get.mockResolvedValue(JSON.stringify(cachedUser));
const result = await userService.getUser('1');
expect(result).toEqual(cachedUser);
expect(mockCache.get).toHaveBeenCalledWith('user:1');
expect(mockApiClient.get).not.toHaveBeenCalled();
});
it('should fetch user from API if not cached', async () => {
const user = { id: '1', name: 'API User' };
mockCache.get.mockResolvedValue(null);
mockApiClient.get.mockResolvedValue(user);
const result = await userService.getUser('1');
expect(result).toEqual(user);
expect(mockApiClient.get).toHaveBeenCalledWith('/users/1');
expect(mockCache.set).toHaveBeenCalledWith(
'user:1',
JSON.stringify(user),
3600
);
});
});
describe('createUser', () => {
it('should create a new user', async () => {
const newUser = { id: '2', name: 'New User', email: '[email protected]' };
mockApiClient.post.mockResolvedValue(newUser);
const result = await userService.createUser({
name: 'New User',
email: '[email protected]',
});
expect(result).toEqual(newUser);
expect(mockApiClient.post).toHaveBeenCalledWith('/users', {
name: 'New User',
email: '[email protected]',
});
});
});
});
import { vi, describe, it, expect } from 'vitest';
describe('Function mocking', () => {
it('should mock a function', () => {
const mockFn = vi.fn();
mockFn.mockReturnValue(42);
expect(mockFn()).toBe(42);
expect(mockFn).toHaveBeenCalledOnce();
});
it('should mock implementation', () => {
const mockFn = vi.fn((x: number) => x * 2);
expect(mockFn(5)).toBe(10);
expect(mockFn).toHaveBeenCalledWith(5);
});
it('should mock resolved value', async () => {
const mockFn = vi.fn();
mockFn.mockResolvedValue({ id: 1, name: 'Test' });
const result = await mockFn();
expect(result).toEqual({ id: 1, name: 'Test' });
});
it('should mock rejected value', async () => {
const mockFn = vi.fn();
mockFn.mockRejectedValue(new Error('Failed'));
await expect(mockFn()).rejects.toThrow('Failed');
});
});
// __mocks__/axios.ts
import { vi } from 'vitest';
export default {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
delete: vi.fn(),
create: vi.fn(() => ({
get: vi.fn(),
post: vi.fn(),
})),
};
// api.test.ts
import { describe, it, expect, vi } from 'vitest';
vi.mock('axios');
import axios from 'axios';
import { fetchUser } from './api';
describe('API', () => {
it('should fetch user data', async () => {
const mockUser = { id: 1, name: 'Test User' };
vi.mocked(axios.get).mockResolvedValue({ data: mockUser });
const user = await fetchUser('1');
expect(user).toEqual(mockUser);
expect(axios.get).toHaveBeenCalledWith('/api/users/1');
});
});
import { vi } from 'vitest';
// Mock only specific exports
vi.mock('./utils', async () => {
const actual = await vi.importActual('./utils');
return {
...actual,
fetchData: vi.fn(),
};
});
import { vi, describe, it, expect } from 'vitest';
describe('Spying', () => {
it('should spy on console.error', () => {
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
console.error('Test error');
expect(spy).toHaveBeenCalledWith('Test error');
spy.mockRestore();
});
it('should spy on object method', () => {
const obj = {
method: (x: number) => x * 2,
};
const spy = vi.spyOn(obj, 'method');
obj.method(5);
expect(spy).toHaveBeenCalledWith(5);
expect(spy).toHaveReturnedWith(10);
});
});
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
describe('Timer tests', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should debounce function calls', () => {
const fn = vi.fn();
const debounced = debounce(fn, 300);
debounced();
debounced();
debounced();
expect(fn).not.toHaveBeenCalled();
vi.advanceTimersByTime(300);
expect(fn).toHaveBeenCalledOnce();
});
it('should throttle function calls', () => {
const fn = vi.fn();
const throttled = throttle(fn, 100);
throttled();
expect(fn).toHaveBeenCalledOnce();
throttled();
expect(fn).toHaveBeenCalledOnce(); // still once
vi.advanceTimersByTime(100);
throttled();
expect(fn).toHaveBeenCalledTimes(2);
});
it('should handle setTimeout', () => {
const callback = vi.fn();
setTimeout(callback, 1000);
expect(callback).not.toHaveBeenCalled();
vi.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalledOnce();
});
});
import { describe, it, expect } from 'vitest';
describe('Snapshot tests', () => {
it('should match snapshot', () => {
const data = {
id: 1,
name: 'Test User',
roles: ['admin', 'user'],
createdAt: new Date('2024-01-01'),
};
expect(data).toMatchSnapshot();
});
it('should match inline snapshot', () => {
const formatted = formatUserName({ first: 'John', last: 'Doe' });
expect(formatted).toMatchInlineSnapshot(`"John Doe"`);
});
it('should match file snapshot', () => {
const html = renderComponent({ title: 'Test', count: 5 });
expect(html).toMatchFileSnapshot('./__snapshots__/component.html');
});
});
import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button component', () => {
it('should render with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('should handle click events', () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(onClick).toHaveBeenCalledOnce();
});
it('should be disabled when disabled prop is true', () => {
render(<Button disabled>Click me</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
it('should apply variant classes', () => {
render(<Button variant="primary">Primary</Button>);
expect(screen.getByRole('button')).toHaveClass('btn-primary');
});
});
import { describe, it, expect } from 'vitest';
describe('Async tests', () => {
it('should handle async/await', async () => {
const result = await fetchData();
expect(result.status).toBe('success');
});
it('should handle promises with resolves', async () => {
await expect(fetchUser('1')).resolves.toEqual({
id: '1',
name: 'Test User',
});
});
it('should handle promise rejections', async () => {
await expect(fetchUser('invalid')).rejects.toThrow('User not found');
});
it('should test multiple async operations', async () => {
const [user, posts] = await Promise.all([
fetchUser('1'),
fetchPosts('1'),
]);
expect(user.id).toBe('1');
expect(posts).toHaveLength(5);
});
});
import { describe, it, expect, beforeEach } from 'vitest';
describe('User operations', () => {
let testUser: User;
beforeEach<{ user: User }>(async (context) => {
// Setup runs before each test
testUser = await createTestUser();
context.user = testUser;
});
it<{ user: User }>('should update user name', async ({ user }) => {
await updateUserName(user.id, 'New Name');
const updated = await getUser(user.id);
expect(updated.name).toBe('New Name');
});
it<{ user: User }>('should delete user', async ({ user }) => {
await deleteUser(user.id);
await expect(getUser(user.id)).rejects.toThrow('Not found');
});
});
// vitest.config.ts
export default defineConfig({
test: {
browser: {
enabled: true,
name: 'chromium',
provider: 'playwright',
headless: true,
},
},
});
// button.browser.test.ts
import { describe, it, expect } from 'vitest';
import { page } from '@vitest/browser/context';
describe('Button in real browser', () => {
it('should interact with button', async () => {
await page.goto('/button-demo');
const button = await page.locator('button');
await button.click();
const counter = await page.locator('#counter');
await expect(counter).toHaveText('1');
});
});
globals: true or explicit imports.*.test.ts files next to source files for quick access.test.skip or .only.# Run all tests
vitest
# Run in watch mode (default)
vitest watch
# Run once (CI mode)
vitest run
# Run specific file
vitest run src/utils/validators.test.ts
# Run with UI
vitest --ui
# Run with coverage
vitest --coverage
# Run tests matching pattern
vitest --reporter=verbose --grep="user"
# Run in browser mode
vitest --browser
# Run benchmarks
vitest bench
// Use test.only to isolate tests
it.only('should debug this test', () => {
// debugger; // Breakpoint
expect(true).toBe(true);
});
// Use console.log for quick debugging
it('should log values', () => {
const value = computeValue();
console.log('Computed value:', value);
expect(value).toBe(42);
});
// tests/setup.ts
import { expect } from 'vitest';
expect.extend({
toBeWithinRange(received: number, floor: number, ceiling: number) {
const pass = received >= floor && received <= ceiling;
return {
pass,
message: () =>
`expected ${received} to be within range ${floor} - ${ceiling}`,
};
},
});
declare module 'vitest' {
interface Assertion<T = any> {
toBeWithinRange(floor: number, ceiling: number): T;
}
}
Vitest automatically uses your Vite config, including plugins like:
@vitejs/plugin-react for React JSX supportvite-tsconfig-paths for TypeScript path mappingThis makes Vitest the natural choice for Vite-based projects.
testing
Teaches the agent to migrate a Jest suite to Vitest — vi.mock and the globals shim, vitest.config workspaces/projects, coverage, browser mode, and Vitest v4 breaking changes.
testing
Teaches the agent to speed up Node integration tests with Testcontainers reuse — withReuse(true), TESTCONTAINERS_REUSE_ENABLE, the .testcontainers.properties opt-in, stable hashing for Postgres/MySQL/Kafka, and Ryuk/CI caveats.
development
Port a Java Selenium suite to Playwright TypeScript - locator mapping, WebDriverWait to auto-wait, Grid to workers, Page Object port, with before/after code and a phased checklist.
development
Gate RAG pipelines in CI with versioned golden eval sets, per-metric thresholds, baseline drift detection, and a build that fails when retrieval or answer quality regresses.