Research-mind toolchains-typescript-testing-jest
Jest + TypeScript - Industry Standard Testing
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-jest" ~/.claude/skills/macphobos-research-mind-toolchains-typescript-testing-jest && rm -rf "$T"
.claude/skills/toolchains-typescript-testing-jest/skill.mdJest + TypeScript - Industry Standard Testing
Overview
Jest is the industry-standard testing framework with 70% market share, providing a mature, battle-tested ecosystem for TypeScript projects. It offers comprehensive testing capabilities with built-in snapshot testing, mocking, and coverage reporting.
Key Features:
- 🏆 Industry Standard: 70% market share, widely adopted
- 📦 All-in-One: Test runner, assertions, mocks, coverage in one package
- 📸 Snapshot Testing: Built-in snapshot support for UI testing
- 🧪 React Integration: React Testing Library, enzyme compatibility
- 🔧 Mature Ecosystem: Extensive plugins, tooling, and community support
- 🎯 TypeScript Support: Full type safety via ts-jest
- 🔍 Coverage Reports: Built-in Istanbul coverage
- 🌐 Multi-Platform: Node.js, browser (jsdom), React Native
Installation:
npm install -D jest @types/jest ts-jest npm install -D @testing-library/react @testing-library/jest-dom # For React
Basic Setup
1. Initialize Jest Configuration
npx ts-jest config:init
This creates jest.config.js:
module.exports = { preset: 'ts-jest', testEnvironment: 'node', };
2. Manual Configuration
jest.config.ts (TypeScript config):
import type { Config } from 'jest'; const config: Config = { preset: 'ts-jest', testEnvironment: 'node', roots: ['<rootDir>/src'], testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], collectCoverageFrom: [ 'src/**/*.{ts,tsx}', '!src/**/*.d.ts', '!src/**/*.test.{ts,tsx}', '!src/**/__tests__/**', ], coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80, }, }, }; export default config;
3. TypeScript Configuration
tsconfig.json:
{ "compilerOptions": { "types": ["jest", "@testing-library/jest-dom"], "esModuleInterop": true } }
tsconfig.test.json (test-specific):
{ "extends": "./tsconfig.json", "compilerOptions": { "types": ["jest", "node", "@testing-library/jest-dom"] }, "include": ["src/**/*.test.ts", "src/**/*.spec.ts", "src/**/__tests__/**"] }
4. Package.json Scripts
{ "scripts": { "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage", "test:ci": "jest --ci --coverage --maxWorkers=2" } }
Core Testing Patterns
Basic Test Structure
import { describe, it, expect, beforeEach, afterEach } from '@jest/globals'; describe('Calculator', () => { let calculator: Calculator; beforeEach(() => { calculator = new Calculator(); }); afterEach(() => { // Cleanup }); 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); }); it.each([ [1, 1, 2], [2, 3, 5], [10, -5, 5], ])('adds %i + %i to equal %i', (a, b, expected) => { expect(calculator.add(a, b)).toBe(expected); }); });
TypeScript Type-Safe Tests
interface User { id: number; name: string; email: string; role: 'admin' | 'user'; } describe('User Service', () => { it('creates user with correct types', () => { const user: User = { id: 1, name: 'Alice', email: 'alice@example.com', role: 'admin', }; // Type-safe assertions expect(user.id).toEqual(expect.any(Number)); expect(user.name).toEqual(expect.any(String)); expect(user.role).toMatch(/^(admin|user)$/); }); it('validates user object shape', () => { const user = createUser('Bob', 'bob@example.com'); expect(user).toMatchObject({ id: expect.any(Number), name: 'Bob', email: 'bob@example.com', }); }); });
Mocking with TypeScript
jest.mock for Module Mocking
import { jest } from '@jest/globals'; import { UserService } from './UserService'; import * as userApi from './api/userApi'; // Mock entire module jest.mock('./api/userApi'); describe('UserService with Mocks', () => { beforeEach(() => { jest.clearAllMocks(); }); it('fetches user data', async () => { const mockUser = { id: 1, name: 'Alice', email: 'alice@example.com' }; // Type-safe mock const mockedFetchUser = jest.mocked(userApi.fetchUser); mockedFetchUser.mockResolvedValue(mockUser); const service = new UserService(); const user = await service.getUser(1); expect(mockedFetchUser).toHaveBeenCalledWith(1); expect(user).toEqual(mockUser); }); });
jest.spyOn for Method Spying
import { jest } from '@jest/globals'; class Logger { log(message: string): void { console.log(message); } error(message: string): void { console.error(message); } } describe('Logger Spy', () => { let logger: Logger; let logSpy: jest.SpyInstance; beforeEach(() => { logger = new Logger(); logSpy = jest.spyOn(logger, 'log'); }); afterEach(() => { logSpy.mockRestore(); }); it('tracks method calls', () => { logger.log('Hello'); logger.log('World'); expect(logSpy).toHaveBeenCalledTimes(2); expect(logSpy).toHaveBeenCalledWith('Hello'); expect(logSpy).toHaveBeenLastCalledWith('World'); }); it('provides custom implementation', () => { logSpy.mockImplementation((msg: string) => { console.log(`[CUSTOM] ${msg}`); }); logger.log('Test'); expect(logSpy).toHaveBeenCalledWith('Test'); }); });
Type-Safe Mock Functions
import { jest } from '@jest/globals'; interface ApiResponse<T> { data: T; status: number; } type FetchUserFn = (id: number) => Promise<ApiResponse<User>>; describe('Type-Safe Mocks', () => { it('creates typed mock function', async () => { const mockFetchUser = jest.fn<FetchUserFn>() .mockResolvedValue({ data: { id: 1, name: 'Alice', email: 'alice@example.com', role: 'user' }, status: 200, }); const result = await mockFetchUser(1); expect(result.data.name).toBe('Alice'); expect(result.status).toBe(200); expect(mockFetchUser).toHaveBeenCalledWith(1); }); it('uses mock implementation', () => { const mockCalculate = jest.fn<(x: number, y: number) => number>() .mockImplementation((x, y) => x + y); expect(mockCalculate(5, 3)).toBe(8); expect(mockCalculate).toHaveBeenCalledWith(5, 3); }); });
Mocking Timers
import { jest } from '@jest/globals'; describe('Timer Mocking', () => { beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.useRealTimers(); }); it('fast-forwards time', () => { const callback = jest.fn(); setTimeout(callback, 1000); jest.advanceTimersByTime(500); expect(callback).not.toHaveBeenCalled(); jest.advanceTimersByTime(500); expect(callback).toHaveBeenCalledTimes(1); }); it('runs all timers', () => { const callback = jest.fn(); setTimeout(callback, 1000); setTimeout(callback, 2000); jest.runAllTimers(); expect(callback).toHaveBeenCalledTimes(2); }); it('handles intervals', () => { const callback = jest.fn(); setInterval(callback, 1000); jest.advanceTimersByTime(3500); expect(callback).toHaveBeenCalledTimes(3); jest.clearAllTimers(); }); });
React Testing Library + TypeScript
Setup for React
npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event npm install -D jest-environment-jsdom
jest.config.ts (React):
import type { Config } from 'jest'; const config: Config = { preset: 'ts-jest', testEnvironment: 'jsdom', setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts'], moduleNameMapper: { '\\.(css|less|scss|sass)$': 'identity-obj-proxy', '\\.(jpg|jpeg|png|gif|svg)$': '<rootDir>/__mocks__/fileMock.js', }, transform: { '^.+\\.tsx?$': ['ts-jest', { tsconfig: { jsx: 'react-jsx', }, }], }, }; export default config;
src/test/setup.ts:
import '@testing-library/jest-dom'; import { cleanup } from '@testing-library/react'; import { afterEach } from '@jest/globals'; afterEach(() => { cleanup(); });
React Component Testing
import { render, screen, waitFor } 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 with correct value', async () => { const onChange = jest.fn(); const user = userEvent.setup(); render(<Counter initialCount={5} onChange={onChange} />); await user.click(screen.getByRole('button', { name: /increment/i })); expect(onChange).toHaveBeenCalledWith(6); expect(onChange).toHaveBeenCalledTimes(1); }); it('disables button when max count reached', () => { render(<Counter initialCount={10} maxCount={10} />); const button = screen.getByRole('button', { name: /increment/i }); expect(button).toBeDisabled(); }); });
Testing Hooks
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('decrements counter', () => { const { result } = renderHook(() => useCounter(5)); act(() => { result.current.decrement(); }); expect(result.current.count).toBe(4); }); it('resets to initial value', () => { const { result } = renderHook(() => useCounter(10)); act(() => { result.current.increment(); result.current.increment(); }); expect(result.current.count).toBe(12); act(() => { result.current.reset(); }); expect(result.current.count).toBe(10); }); });
Testing Async Components
import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { UserProfile } from './UserProfile'; import * as api from './api'; jest.mock('./api'); describe('UserProfile Async', () => { it('loads and displays user data', async () => { const mockUser = { id: 1, name: 'Alice', email: 'alice@example.com' }; jest.mocked(api.fetchUser).mockResolvedValue(mockUser); render(<UserProfile userId={1} />); expect(screen.getByText('Loading...')).toBeInTheDocument(); await waitFor(() => { expect(screen.getByText('Alice')).toBeInTheDocument(); }); expect(screen.getByText('alice@example.com')).toBeInTheDocument(); }); it('displays error on fetch failure', async () => { jest.mocked(api.fetchUser).mockRejectedValue(new Error('Network error')); render(<UserProfile userId={1} />); await waitFor(() => { expect(screen.getByText(/error/i)).toBeInTheDocument(); }); }); });
Snapshot Testing
Component Snapshots
import { render } from '@testing-library/react'; import { UserCard } from './UserCard'; describe('UserCard Snapshots', () => { it('matches snapshot for regular user', () => { const { container } = render( <UserCard name="Alice" email="alice@example.com" role="user" /> ); expect(container.firstChild).toMatchSnapshot(); }); it('matches snapshot for admin user', () => { const { container } = render( <UserCard name="Bob" email="bob@example.com" role="admin" /> ); expect(container.firstChild).toMatchSnapshot(); }); it('uses inline snapshot', () => { const user = { id: 1, name: 'Charlie', role: 'user' }; expect(user).toMatchInlineSnapshot(` { "id": 1, "name": "Charlie", "role": "user", } `); }); });
Updating Snapshots
# Update all snapshots jest --updateSnapshot jest -u # Update snapshots for specific test file jest UserCard.test.tsx -u # Interactive snapshot update jest --watch # Press 'u' to update failing snapshots
Custom Snapshot Serializers
// __tests__/serializers/dateSerializer.ts export default { test: (val: any) => val instanceof Date, print: (val: Date) => `Date(${val.toISOString()})`, };
jest.config.ts:
const config: Config = { snapshotSerializers: ['<rootDir>/__tests__/serializers/dateSerializer.ts'], };
Async Testing
Testing Promises
import { fetchData, saveData } from './api'; describe('Async Operations', () => { it('resolves with data', async () => { const data = await fetchData(1); expect(data).toBeDefined(); expect(data.id).toBe(1); }); it('handles promise rejection', async () => { await expect(fetchData(-1)).rejects.toThrow('Invalid ID'); }); it('uses resolves matcher', async () => { await expect(fetchData(1)).resolves.toHaveProperty('id', 1); }); it('tests multiple async operations', async () => { const [user, posts] = await Promise.all([ fetchUser(1), fetchPosts(1), ]); expect(user.id).toBe(1); expect(posts).toHaveLength(expect.any(Number)); }); });
Testing Callbacks
describe('Callback Testing', () => { it('calls callback with correct arguments', (done) => { function fetchWithCallback(id: number, callback: (data: any) => void) { setTimeout(() => { callback({ id, name: 'Test' }); }, 100); } fetchWithCallback(1, (data) => { try { expect(data.id).toBe(1); expect(data.name).toBe('Test'); done(); } catch (error) { done(error); } }); }); });
Coverage Configuration
Advanced Coverage Setup
jest.config.ts:
const config: Config = { collectCoverage: true, coverageDirectory: 'coverage', coverageProvider: 'v8', // or 'babel' for compatibility coverageReporters: ['text', 'lcov', 'html', 'json'], collectCoverageFrom: [ 'src/**/*.{ts,tsx}', '!src/**/*.d.ts', '!src/**/*.test.{ts,tsx}', '!src/**/__tests__/**', '!src/index.ts', '!src/types/**', ], coverageThreshold: { global: { branches: 80, functions: 80, lines: 80, statements: 80, }, './src/core/': { branches: 90, functions: 90, lines: 90, statements: 90, }, }, coveragePathIgnorePatterns: [ '/node_modules/', '/dist/', '/__tests__/', ], };
Running Coverage
# Generate coverage report npm test -- --coverage # Coverage with watch mode npm test -- --coverage --watch # Coverage for specific files npm test -- --coverage --collectCoverageFrom="src/components/**/*.tsx" # View HTML report open coverage/lcov-report/index.html
Migration from Vitest
Key Differences
API Changes:
// Vitest import { vi } from 'vitest'; const mockFn = vi.fn(); vi.spyOn(obj, 'method'); // Jest import { jest } from '@jest/globals'; const mockFn = jest.fn(); jest.spyOn(obj, 'method');
Migration Checklist
1. Update Dependencies:
npm uninstall vitest @vitest/ui npm install -D jest @types/jest ts-jest
2. Update package.json:
{ "scripts": { "test": "jest", // Was: vitest run "test:watch": "jest --watch" // Was: vitest } }
3. Replace vitest.config.ts with jest.config.ts:
// Old: vitest.config.ts import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { globals: true, environment: 'jsdom', }, }); // New: jest.config.ts import type { Config } from 'jest'; const config: Config = { preset: 'ts-jest', testEnvironment: 'jsdom', globals: { 'ts-jest': { isolatedModules: true, }, }, }; export default config;
4. Update Test Files:
// Change imports - import { vi } from 'vitest'; + import { jest } from '@jest/globals'; // Update mocks - vi.fn() + jest.fn() - vi.spyOn() + jest.spyOn() - vi.mock() + jest.mock() // Timer mocks - vi.useFakeTimers() + jest.useFakeTimers() - vi.advanceTimersByTime() + jest.advanceTimersByTime()
5. Update tsconfig.json:
{ "compilerOptions": { "types": ["jest", "@testing-library/jest-dom"] // Was: vitest/globals } }
Jest vs Vitest Comparison
Performance
Jest:
- Slower initial startup (no HMR)
- Sequential test execution by default
- 1-5 seconds for medium projects
Vitest:
- Instant HMR-based execution
- Parallel by default
- 100-500ms for same projects
Ecosystem
Jest:
- ✅ 70% market share
- ✅ Mature ecosystem (8+ years)
- ✅ More Stack Overflow answers
- ✅ Better corporate support
Vitest:
- ✅ Modern, growing adoption
- ✅ Vite-native integration
- ⚠️ Smaller ecosystem
- ⚠️ Fewer resources
TypeScript Support
Jest:
- Requires ts-jest configuration
- Extra transform step
- Slower compilation
Vitest:
- Built-in TypeScript support
- No configuration needed
- Faster through Vite
When to Use Jest
Choose Jest for:
- ✅ Existing projects already using Jest
- ✅ Corporate environments requiring proven tools
- ✅ Projects requiring extensive ecosystem support
- ✅ React projects with Create React App
- ✅ Non-Vite build systems (Webpack, Rollup)
Choose Vitest for:
- ✅ New projects with modern tooling
- ✅ Vite-based applications
- ✅ Performance-critical test suites
- ✅ ESM-first projects
Best Practices
- Use TypeScript Configuration: Type-safe tests prevent runtime errors
- Mock External Dependencies: Network, file system, databases
- Isolate Tests: Each test should be independent
- Use describe Blocks: Group related tests logically
- Clear Mock State: Use
injest.clearAllMocks()beforeEach - Test Edge Cases: Empty arrays, null, undefined, errors
- Use .each for Data-Driven Tests: Test multiple inputs efficiently
- Avoid Testing Implementation: Test behavior, not internal structure
- Keep Tests Fast: Mock slow operations, use parallel execution
- Maintain Coverage Thresholds: Enforce minimum coverage in CI
Common Pitfalls
❌ Not clearing mocks between tests:
// WRONG - mocks leak between tests it('test 1', () => { jest.spyOn(api, 'fetch'); // No cleanup! }); // CORRECT afterEach(() => { jest.restoreAllMocks(); });
❌ Forgetting to await async tests:
// WRONG - test completes 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(); });
❌ Using wrong test environment:
// WRONG - testing DOM without jsdom // jest.config.ts testEnvironment: 'node', // Can't test React! // CORRECT testEnvironment: 'jsdom',
❌ Not using TypeScript types for mocks:
// WRONG - no type safety const mockFn = jest.fn(); // CORRECT const mockFn = jest.fn<(id: number) => Promise<User>>();
Resources
- Documentation: https://jestjs.io/docs/getting-started
- TypeScript Guide: https://jestjs.io/docs/getting-started#using-typescript
- ts-jest: https://kulshekhar.github.io/ts-jest/
- React Testing Library: https://testing-library.com/docs/react-testing-library/intro/
- Jest DOM Matchers: https://github.com/testing-library/jest-dom
Related Skills
When using Jest, consider these complementary skills:
- typescript-core: Advanced TypeScript patterns, tsconfig optimization, and type safety
- react: React component testing patterns with Testing Library
- vitest: Modern alternative with Vite-native performance and faster execution
Quick TypeScript Type Safety Reference (Inlined for Standalone Use)
// Type-safe test helpers with generics function createMockUser<T extends Partial<User>>(overrides: T): User & T { return { id: 1, name: 'Test User', email: 'test@example.com', ...overrides }; } // Usage with type inference const adminUser = createMockUser({ role: 'admin' }); // Type: User & { role: string } // Type-safe mock functions const mockFetch = jest.fn<typeof fetch>(); mockFetch.mockResolvedValue(new Response('{}')); // Const type parameters for literal types const createConfig = <const T extends Record<string, unknown>>(config: T): T => config; const testConfig = createConfig({ environment: 'test', debug: true }); // Type: { environment: "test"; debug: true } (literals preserved)
Quick React Testing Patterns (Inlined for Standalone Use)
// React Testing Library with Jest import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import '@testing-library/jest-dom'; // Component testing pattern describe('UserProfile', () => { it('should display 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(); }); it('should handle user interactions', async () => { const onSubmit = jest.fn(); render(<UserForm onSubmit={onSubmit} />); // User interactions await userEvent.type(screen.getByLabelText('Name'), 'Bob'); await userEvent.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', () => { const { result } = renderHook(() => useCounter(0)); expect(result.current.count).toBe(0); act(() => { result.current.increment(); }); expect(result.current.count).toBe(1); }); // Context and Provider testing const wrapper = ({ children }: { children: React.ReactNode }) => ( <AuthProvider>{children}</AuthProvider> ); test('useAuth hook with context', () => { const { result } = renderHook(() => useAuth(), { wrapper }); expect(result.current.user).toBeDefined(); });
Quick Vitest Comparison (Inlined for Standalone Use)
When to Choose Vitest over Jest:
- New Vite/Vite-based projects (Next.js with Turbopack, SvelteKit)
- Need faster test execution (10-100x faster)
- ESM-first architecture
- Hot Module Replacement for tests
When to Stick with Jest:
- Existing large codebases with Jest already configured
- Corporate environments with established Jest workflows
- Need mature ecosystem and extensive plugins
- React apps with Create React App (default Jest setup)
Migration Snippet (Jest → Vitest):
// Jest: import from '@testing-library/jest-dom' import '@testing-library/jest-dom'; // Vitest: import from vitest globals import { expect, test, describe } from 'vitest'; import { screen } from '@testing-library/react'; // Most Jest syntax works in Vitest unchanged test('component renders', () => { render(<Component />); expect(screen.getByText('Hello')).toBeTruthy(); });
[Full TypeScript, React, and Vitest patterns available in respective skills if deployed together]
Summary
- Jest is the industry standard with 70% market share
- TypeScript support via ts-jest with full type safety
- All-in-one solution: Test runner, assertions, mocks, coverage
- React Testing Library integration for component testing
- Mature ecosystem with extensive tooling and support
- Snapshot testing for UI regression testing
- Migration path from Vitest with compatible API
- Perfect for: Existing projects, corporate environments, React apps, legacy support
- Trade-off: Slower than Vitest but more mature and widely supported