git clone https://github.com/MacPhobos/research-mind
T=$(mktemp -d) && git clone --depth=1 https://github.com/MacPhobos/research-mind "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/toolchains-typescript-testing-vitest" ~/.claude/skills/macphobos-research-mind-toolchains-typescript-testing-vitest && rm -rf "$T"
.claude/skills/toolchains-typescript-testing-vitest/skill.mdVitest - Modern TypeScript Testing
Overview
Vitest is a next-generation test framework powered by Vite, designed for modern TypeScript/JavaScript projects. It provides blazing-fast test execution through HMR-based test running, native ESM support, and first-class TypeScript integration.
Key Features:
- ⚡ Vite-native: Instant HMR-based test execution (10-100x faster than Jest)
- 🎯 TypeScript-first: Built-in TypeScript support, no configuration needed
- 🔄 ESM-native: Native ES modules, async/await, top-level await
- 🧪 Jest-compatible: Compatible API for easy migration
- 📸 Snapshot testing: Built-in snapshot support
- 🎨 Component testing: React Testing Library, Vue Test Utils integration
- 📊 Coverage: Built-in v8/c8 coverage (faster than Istanbul)
- 🌐 UI mode: Beautiful web UI for test debugging
Installation:
npm install -D vitest # TypeScript types (usually auto-detected) npm install -D @vitest/ui # Optional: UI mode
Basic Setup
1. Configure Vitest
vitest.config.ts:
import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, // Use describe/it/expect globally environment: 'node', // or 'jsdom' for DOM testing coverage: { provider: 'v8', // or 'istanbul' reporter: ['text', 'json', 'html'], exclude: [ 'node_modules/', 'dist/', '**/*.test.ts', '**/*.spec.ts', ], }, include: ['**/*.{test,spec}.{ts,tsx}'], exclude: ['node_modules', 'dist', '.idea', '.git', '.cache'], }, });
2. TypeScript Configuration
tsconfig.json:
{ "compilerOptions": { "types": ["vitest/globals"] // For global describe/it/expect } }
Alternative (without globals):
import { describe, it, expect } from 'vitest';
3. Package.json Scripts
{ "scripts": { "test": "vitest run", // CI mode (single run) "test:watch": "vitest", // Watch mode (default) "test:ui": "vitest --ui", // UI mode "test:coverage": "vitest run --coverage" } }
Core Testing Patterns
Basic Test Structure
import { describe, it, expect, beforeEach, afterEach } from 'vitest'; describe('Calculator', () => { let calculator: Calculator; beforeEach(() => { calculator = new Calculator(); }); it('adds two numbers correctly', () => { const result = calculator.add(2, 3); expect(result).toBe(5); }); it('handles negative numbers', () => { expect(calculator.add(-5, 3)).toBe(-2); }); });
TypeScript Type Testing
import { describe, it, expectTypeOf, assertType } from 'vitest'; interface User { id: number; name: string; email: string; } describe('Type Safety', () => { it('ensures correct types', () => { const user: User = { id: 1, name: 'Alice', email: 'alice@example.com', }; // Type assertions expectTypeOf(user.id).toBeNumber(); expectTypeOf(user.name).toBeString(); expectTypeOf(user).toMatchTypeOf<User>(); // Assert type at compile time assertType<User>(user); }); it('checks function return types', () => { function getUser(): User { return { id: 1, name: 'Bob', email: 'bob@example.com' }; } expectTypeOf(getUser).returns.toMatchTypeOf<User>(); }); });
Mocking and Spies
vi.mock for Module Mocking
import { describe, it, expect, vi } from 'vitest'; import { fetchUser } from './api'; import { UserService } from './UserService'; // Mock entire module vi.mock('./api', () => ({ fetchUser: vi.fn(), })); describe('UserService', () => { it('fetches user data', async () => { const mockUser = { id: 1, name: 'Alice' }; vi.mocked(fetchUser).mockResolvedValue(mockUser); const service = new UserService(); const user = await service.getUser(1); expect(fetchUser).toHaveBeenCalledWith(1); expect(user).toEqual(mockUser); }); });
vi.spyOn for Method Spying
import { describe, it, expect, vi } from 'vitest'; class Logger { log(message: string) { console.log(message); } } describe('Logger Spy', () => { it('tracks method calls', () => { const logger = new Logger(); const spy = vi.spyOn(logger, 'log'); logger.log('Hello'); logger.log('World'); expect(spy).toHaveBeenCalledTimes(2); expect(spy).toHaveBeenCalledWith('Hello'); expect(spy).toHaveBeenLastCalledWith('World'); spy.mockRestore(); // Restore original implementation }); });
Mock Implementation
import { describe, it, expect, vi } from 'vitest'; describe('Mock Implementation', () => { it('provides custom mock implementation', () => { const mockFn = vi.fn((x: number) => x * 2); expect(mockFn(5)).toBe(10); expect(mockFn).toHaveBeenCalledWith(5); // Change implementation mockFn.mockImplementation((x: number) => x + 10); expect(mockFn(5)).toBe(15); // One-time implementation mockFn.mockImplementationOnce((x: number) => 100); expect(mockFn(5)).toBe(100); expect(mockFn(5)).toBe(15); // Back to default }); });
Mocking Timers
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; describe('Timer Mocking', () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.restoreAllMocks(); }); it('fast-forwards time', () => { const callback = vi.fn(); setTimeout(callback, 1000); vi.advanceTimersByTime(500); expect(callback).not.toHaveBeenCalled(); vi.advanceTimersByTime(500); expect(callback).toHaveBeenCalledTimes(1); }); it('runs all timers', async () => { const callback = vi.fn(); setTimeout(callback, 1000); setTimeout(callback, 2000); await vi.runAllTimersAsync(); expect(callback).toHaveBeenCalledTimes(2); }); });
React Testing Integration
Setup React Testing Library
npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event npm install -D jsdom # For DOM environment
vitest.config.ts (React):
import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], test: { globals: true, environment: 'jsdom', setupFiles: './src/test/setup.ts', }, });
src/test/setup.ts:
import '@testing-library/jest-dom'; import { expect, afterEach } from 'vitest'; import { cleanup } from '@testing-library/react'; import * as matchers from '@testing-library/jest-dom/matchers'; expect.extend(matchers); afterEach(() => { cleanup(); });
React Component Testing
import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Counter } from './Counter'; describe('Counter Component', () => { it('renders initial count', () => { render(<Counter initialCount={0} />); expect(screen.getByText('Count: 0')).toBeInTheDocument(); }); it('increments counter on button click', async () => { const user = userEvent.setup(); render(<Counter initialCount={0} />); const button = screen.getByRole('button', { name: /increment/i }); await user.click(button); expect(screen.getByText('Count: 1')).toBeInTheDocument(); }); it('calls onChange callback', async () => { const onChange = vi.fn(); const user = userEvent.setup(); render(<Counter initialCount={0} onChange={onChange} />); await user.click(screen.getByRole('button', { name: /increment/i })); expect(onChange).toHaveBeenCalledWith(1); }); });
Testing Hooks
import { describe, it, expect } from 'vitest'; import { renderHook, act } from '@testing-library/react'; import { useCounter } from './useCounter'; describe('useCounter Hook', () => { it('initializes with default value', () => { const { result } = renderHook(() => useCounter(0)); expect(result.current.count).toBe(0); }); it('increments counter', () => { const { result } = renderHook(() => useCounter(0)); act(() => { result.current.increment(); }); expect(result.current.count).toBe(1); }); it('resets counter', () => { const { result } = renderHook(() => useCounter(10)); act(() => { result.current.reset(); }); expect(result.current.count).toBe(10); }); });
Vue Testing Integration
Setup Vue Test Utils
npm install -D @vue/test-utils @vitejs/plugin-vue npm install -D happy-dom # Faster alternative to jsdom
vitest.config.ts (Vue):
import { defineConfig } from 'vitest/config'; import vue from '@vitejs/plugin-vue'; export default defineConfig({ plugins: [vue()], test: { globals: true, environment: 'happy-dom', setupFiles: './src/test/setup.ts', }, });
Vue Component Testing
import { describe, it, expect } from 'vitest'; import { mount } from '@vue/test-utils'; import Counter from './Counter.vue'; describe('Counter.vue', () => { it('renders initial count', () => { const wrapper = mount(Counter, { props: { initialCount: 5 }, }); expect(wrapper.text()).toContain('Count: 5'); }); it('increments on button click', async () => { const wrapper = mount(Counter, { props: { initialCount: 0 }, }); await wrapper.find('button').trigger('click'); expect(wrapper.text()).toContain('Count: 1'); }); it('emits update event', async () => { const wrapper = mount(Counter, { props: { initialCount: 0 }, }); await wrapper.find('button').trigger('click'); expect(wrapper.emitted('update')).toBeTruthy(); expect(wrapper.emitted('update')?.[0]).toEqual([1]); }); });
Async Testing
Testing Promises
import { describe, it, expect } from 'vitest'; describe('Async Operations', () => { it('resolves promises', async () => { const result = await Promise.resolve(42); expect(result).toBe(42); }); it('rejects promises', async () => { await expect(Promise.reject(new Error('Failed'))).rejects.toThrow('Failed'); }); it('uses resolves matcher', async () => { await expect(Promise.resolve(42)).resolves.toBe(42); }); });
Testing Async Functions
import { describe, it, expect, vi } from 'vitest'; async function fetchData(id: number): Promise<string> { const response = await fetch(`/api/data/${id}`); return response.json(); } describe('Async Functions', () => { it('fetches data successfully', async () => { global.fetch = vi.fn(() => Promise.resolve({ json: () => Promise.resolve('data'), } as Response) ); const data = await fetchData(1); expect(data).toBe('data'); expect(fetch).toHaveBeenCalledWith('/api/data/1'); }); it('handles fetch errors', async () => { global.fetch = vi.fn(() => Promise.reject(new Error('Network error'))); await expect(fetchData(1)).rejects.toThrow('Network error'); }); });
Snapshot Testing
Basic Snapshots
import { describe, it, expect } from 'vitest'; import { render } from '@testing-library/react'; import { UserCard } from './UserCard'; describe('UserCard Snapshots', () => { it('matches snapshot', () => { const { container } = render( <UserCard name="Alice" email="alice@example.com" /> ); expect(container.firstChild).toMatchSnapshot(); }); it('matches inline snapshot', () => { const user = { id: 1, name: 'Bob' }; expect(user).toMatchInlineSnapshot(` { "id": 1, "name": "Bob", } `); }); });
Snapshot Serializers
import { describe, it, expect } from 'vitest'; expect.addSnapshotSerializer({ test: (val) => val && typeof val.toISOString === 'function', print: (val) => `Date(${(val as Date).toISOString()})`, }); describe('Custom Serializers', () => { it('serializes dates consistently', () => { const data = { timestamp: new Date('2024-01-01T00:00:00.000Z'), user: 'Alice', }; expect(data).toMatchSnapshot(); }); });
Coverage Configuration
Advanced Coverage Setup
vitest.config.ts:
import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { coverage: { provider: 'v8', reporter: ['text', 'json', 'html', 'lcov'], reportsDirectory: './coverage', exclude: [ 'node_modules/', 'dist/', '**/*.test.ts', '**/*.spec.ts', '**/*.config.ts', '**/types/', ], thresholds: { lines: 80, functions: 80, branches: 75, statements: 80, }, all: true, // Include untested files in coverage report }, }, });
Running Coverage
# Generate coverage npx vitest run --coverage # Coverage with UI npx vitest --coverage --ui # Specific threshold enforcement npx vitest run --coverage --coverage.lines=90
Migration from Jest
API Compatibility
Vitest provides Jest-compatible API:
// Jest syntax works in Vitest import { describe, it, expect, jest } from 'vitest'; // Note: Use 'vi' instead of 'jest' for new code import { describe, it, expect, vi } from 'vitest'; // Both work, but vi is preferred const mockFn = vi.fn(); // Preferred const mockFn2 = jest.fn(); // Also works
Migration Checklist
1. Update Dependencies:
npm uninstall jest @types/jest ts-jest npm install -D vitest @vitest/ui
2. Update package.json:
{ "scripts": { "test": "vitest run", // Was: jest "test:watch": "vitest" // Was: jest --watch } }
3. Replace jest.config.js with vitest.config.ts:
// Old: jest.config.js module.exports = { preset: 'ts-jest', testEnvironment: 'node', }; // New: vitest.config.ts import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, environment: 'node', }, });
4. Update Test Files:
// Change imports - import { jest } from '@jest/globals'; + import { vi } from 'vitest'; // Update mocks - jest.fn() + vi.fn() - jest.spyOn() + vi.spyOn() - jest.mock() + vi.mock()
Advanced Patterns
Concurrent Testing
import { describe, it, expect } from 'vitest'; describe.concurrent('Parallel Tests', () => { it('test 1', async () => { await slowOperation(); expect(true).toBe(true); }); it('test 2', async () => { await slowOperation(); expect(true).toBe(true); }); // Both tests run in parallel });
Test Context
import { describe, it, expect, beforeEach } from 'vitest'; interface TestContext { user: { id: number; name: string }; api: ApiClient; } describe<TestContext>('With Context', () => { beforeEach((context) => { context.user = { id: 1, name: 'Alice' }; context.api = new ApiClient(); }); it<TestContext>('uses context', ({ user, api }) => { expect(user.name).toBe('Alice'); expect(api).toBeDefined(); }); });
Custom Matchers
import { expect } from 'vitest'; expect.extend({ toBeWithinRange(received: number, floor: number, ceiling: number) { const pass = received >= floor && received <= ceiling; return { pass, message: () => pass ? `expected ${received} not to be within range ${floor} - ${ceiling}` : `expected ${received} to be within range ${floor} - ${ceiling}`, }; }, }); // Usage expect(100).toBeWithinRange(90, 110);
Best Practices
- Use globals: true - Simpler imports, Jest-compatible
- Prefer vi over jest - Use Vitest-native API for new code
- Use v8 coverage - Faster than Istanbul, works with native ESM
- Test in isolation - Each test should be independent
- Mock external dependencies - Network, file system, timers
- Use TypeScript - Full type safety in tests
- Run tests in CI mode - Use
for CI, not watch modevitest run - Leverage UI mode - Debug failing tests visually
- Use describe.concurrent - Parallelize independent tests
- Keep tests focused - One assertion per test when possible
Common Pitfalls
❌ Not using CI mode in CI/CD:
// WRONG - watch mode hangs in CI "test": "vitest" // CORRECT - single run "test": "vitest run"
✅ Correct approach:
{ "scripts": { "test": "vitest run", // CI-safe "test:watch": "vitest", // Development "test:ui": "vitest --ui" // Debugging } }
❌ Forgetting to await async tests:
// WRONG - test passes before assertion it('fetches data', () => { fetchData().then(data => { expect(data).toBeDefined(); // Never runs! }); }); // CORRECT it('fetches data', async () => { const data = await fetchData(); expect(data).toBeDefined(); });
❌ Not cleaning up mocks:
// WRONG - mocks leak between tests it('test 1', () => { vi.spyOn(console, 'log'); // No cleanup! }); // CORRECT import { afterEach } from 'vitest'; afterEach(() => { vi.restoreAllMocks(); });
❌ Using wrong environment:
// WRONG - testing DOM in node environment test: { environment: 'node', // Can't test React components! } // CORRECT test: { environment: 'jsdom', // For React/Vue components }
Resources
- Documentation: https://vitest.dev
- API Reference: https://vitest.dev/api/
- Migration Guide: https://vitest.dev/guide/migration.html
- Examples: https://github.com/vitest-dev/vitest/tree/main/examples
- UI Mode: https://vitest.dev/guide/ui.html
Related Skills
When using Vitest, consider these complementary skills:
- typescript-core: Advanced TypeScript type patterns, tsconfig, and runtime validation
- react: React component testing with Testing Library integration
- test-driven-development: Complete TDD workflow (RED/GREEN/REFACTOR cycle)
Quick TypeScript Type Patterns (Inlined for Standalone Use)
// Type-safe test factories with generics function createMockData<T extends Record<string, unknown>>( defaults: T, overrides?: Partial<T> ): T { return { ...defaults, ...overrides }; } const mockUser = createMockData( { id: 1, name: 'Test', email: 'test@example.com' }, { name: 'Alice' } ); // Runtime validation with Zod in tests import { z } from 'zod'; const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.string().email(), }); test('API returns valid user', async () => { const response = await fetch('/api/user/1'); const data = await response.json(); // Runtime validation + type inference const user = UserSchema.parse(data); expect(user.email).toContain('@'); }); // Const type parameters for literal inference const createTestConfig = <const T extends Record<string, unknown>>(config: T): T => config; const testEnv = createTestConfig({ mode: 'test', debug: false }); // Type: { mode: "test"; debug: false } (literals preserved)
Quick React Testing Patterns (Inlined for Standalone Use)
// React Testing Library with Vitest import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { userEvent } from '@testing-library/user-event'; import { describe, test, expect, vi } from 'vitest'; // Component testing describe('UserProfile', () => { test('renders user information', () => { const user = { id: 1, name: 'Alice', email: 'alice@example.com' }; render(<UserProfile user={user} />); expect(screen.getByText('Alice')).toBeInTheDocument(); expect(screen.getByText('alice@example.com')).toBeInTheDocument(); }); test('handles form submission', async () => { const onSubmit = vi.fn(); render(<UserForm onSubmit={onSubmit} />); const user = userEvent.setup(); await user.type(screen.getByLabelText('Name'), 'Bob'); await user.click(screen.getByRole('button', { name: 'Submit' })); await waitFor(() => { expect(onSubmit).toHaveBeenCalledWith({ name: 'Bob' }); }); }); }); // Hook testing import { renderHook, act } from '@testing-library/react'; test('useCounter hook increments', () => { const { result } = renderHook(() => useCounter(0)); expect(result.current.count).toBe(0); act(() => { result.current.increment(); }); expect(result.current.count).toBe(1); });
Quick TDD Workflow Reference (Inlined for Standalone Use)
RED → GREEN → REFACTOR Cycle:
-
RED Phase: Write Failing Test
test('should authenticate user with valid credentials', () => { const user = { username: 'alice', password: 'secret123' }; const result = authenticate(user); expect(result.isAuthenticated).toBe(true); // This fails because authenticate() doesn't exist yet }); -
GREEN Phase: Make It Pass
function authenticate(user: User): AuthResult { // Minimum code to pass the test if (user.username === 'alice' && user.password === 'secret123') { return { isAuthenticated: true }; } return { isAuthenticated: false }; } -
REFACTOR Phase: Improve Code
function authenticate(user: User): AuthResult { // Clean up while keeping tests green const hashed = hashPassword(user.password); const storedUser = database.getUser(user.username); return { isAuthenticated: storedUser?.passwordHash === hashed }; }
Test Structure: Arrange-Act-Assert (AAA)
test('creates user successfully', async () => { // Arrange: Set up test data const userData = { username: 'alice', email: 'alice@example.com' }; // Act: Perform the action const user = await createUser(userData); // Assert: Verify outcome expect(user.username).toBe('alice'); expect(user.email).toBe('alice@example.com'); });
Vitest-Specific TDD Features:
// Watch mode with HMR (instant feedback) // vitest --watch // UI mode for visual debugging // vitest --ui // Run only changed tests // vitest --changed // Benchmark mode for performance testing import { bench } from 'vitest'; bench('authenticate performance', () => { authenticate({ username: 'alice', password: 'secret' }); });
[Full TypeScript, React, and TDD workflows available in respective skills if deployed together]
Summary
- Vitest is the modern standard for TypeScript testing
- 10-100x faster than Jest through Vite-native HMR
- ESM-first with native module support
- Jest-compatible API for easy migration
- TypeScript-first with built-in type support
- Component testing for React and Vue
- v8 coverage faster than Istanbul
- UI mode for visual test debugging
- Perfect for: Modern TypeScript projects, Vite-based apps, React/Vue components