jest-typescript

安装量: 243
排名: #3608

安装

npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill jest-typescript

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', };

  1. Manual Configuration

jest.config.ts (TypeScript config):

import type { Config } from 'jest';

const config: Config = { preset: 'ts-jest', testEnvironment: 'node', roots: ['/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;

  1. 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/*"] }

  1. 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 { data: T; status: number; }

type FetchUserFn = (id: number) => Promise>;

describe('Type-Safe Mocks', () => { it('creates typed mock function', async () => { const mockFetchUser = jest.fn() .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: ['/src/test/setup.ts'], moduleNameMapper: { '\.(css|less|scss|sass)$': 'identity-obj-proxy', '\.(jpg|jpeg|png|gif|svg)$': '/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(); expect(screen.getByText('Count: 0')).toBeInTheDocument(); });

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: ['/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

  1. Update package.json:

{ "scripts": { "test": "jest", // Was: vitest run "test:watch": "jest --watch" // Was: vitest } }

  1. 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;

  1. 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>(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(); mockFetch.mockResolvedValue(new Response('{}'));

// Const type parameters for literal types const createConfig = >(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();

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 }) => ( {children} );

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

返回排行榜