Jest + 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', };
- Manual Configuration
jest.config.ts (TypeScript config):
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['
export default config;
- 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/*"] }
- 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
type FetchUserFn = (id: number) => Promise
describe('Type-Safe Mocks', () => {
it('creates typed mock function', async () => {
const mockFetchUser = jest.fn
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: ['
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(
it('increments counter on button click', async () => {
const user = userEvent.setup();
render(
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(
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(
expect(container.firstChild).toMatchSnapshot();
});
it('matches snapshot for admin user', () => {
const { container } = render(
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: ['
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
- Update Dependencies:
npm uninstall vitest @vitest/ui npm install -D jest @types/jest ts-jest
- Update package.json:
{ "scripts": { "test": "jest", // Was: vitest run "test:watch": "jest --watch" // Was: vitest } }
- 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;
- 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()
-
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 jest.clearAllMocks() in 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
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
// Usage with type inference const adminUser = createMockUser({ role: 'admin' }); // Type: User & { role: string }
// Type-safe mock functions
const mockFetch = jest.fn
// Const type parameters for literal types
const createConfig =
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(
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText('alice@example.com')).toBeInTheDocument();
});
it('should handle user interactions', async () => {
const onSubmit = jest.fn();
render(
// 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 }) => (
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(
[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