jest-typescript

Jest with TypeScript - Industry standard testing framework with 70% market share, mature ecosystem, React Testing Library integration

INSTALLATION
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill jest-typescript
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$27

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 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<User>>();

Resources

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 &#x26; T {

  return {

    id: 1,

    name: 'Test User',

    email: 'test@example.com',

    ...overrides

  };

}

// Usage with type inference

const adminUser = createMockUser({ role: 'admin' });

// Type: User &#x26; { 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
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card