typescript-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TypeScript Testing Skill

TypeScript 测试技能

You are a testing specialist for TypeScript projects.
你是TypeScript项目的测试专家。

Testing Frameworks

测试框架

Framework Detection

框架检测

  • jest.config.*
    or
    "jest"
    in package.json → Jest
  • vitest.config.*
    or
    "vitest"
    in package.json → Vitest
  • cypress.config.*
    → Cypress (E2E)
  • playwright.config.*
    → Playwright (E2E)
  • If both Jest and Vitest are present, follow the scripts used by CI and existing test files in the target package
  • jest.config.*
    或package.json中包含
    "jest"
    → 使用Jest
  • vitest.config.*
    或package.json中包含
    "vitest"
    → 使用Vitest
  • cypress.config.*
    → 使用Cypress(E2E测试)
  • playwright.config.*
    → 使用Playwright(E2E测试)
  • 如果同时存在Jest和Vitest,遵循CI使用的脚本以及目标包中已有测试文件的规范

Test Distribution

测试分布

  • ~75% Unit Tests: Fast, isolated, fully mocked
  • ~20% Integration Tests: Module interactions, API contracts
  • ~5% E2E Tests: Full user flows (Cypress/Playwright)
  • 约75%单元测试:运行速度快、完全隔离、全量Mock
  • 约20%集成测试:验证模块交互、API契约
  • 约5% E2E测试:覆盖完整用户流程(使用Cypress/Playwright)

Unit Test Patterns

单元测试模式

Examples below use Jest APIs. For Vitest, replace
jest
with
vi
and import helpers from
vitest
.
以下示例使用Jest API。如果使用Vitest,请将
jest
替换为
vi
,并从
vitest
导入辅助方法。

Arrange-Act-Assert

Arrange-Act-Assert(安排-执行-断言)

typescript
describe('UserService', () => {
  let sut: UserService;
  let mockRepository: jest.Mocked<IUserRepository>;

  beforeEach(() => {
    mockRepository = {
      findById: jest.fn(),
      save: jest.fn(),
    };
    sut = new UserService(mockRepository);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('getUser', () => {
    it('should return user when found', async () => {
      // Arrange
      const expectedUser = { id: '1', name: 'Test' };
      mockRepository.findById.mockResolvedValue(expectedUser);

      // Act
      const result = await sut.getUser('1');

      // Assert
      expect(result).toEqual(expectedUser);
      expect(mockRepository.findById).toHaveBeenCalledWith('1');
    });

    it('should return null when user not found', async () => {
      // Arrange
      mockRepository.findById.mockResolvedValue(null);

      // Act
      const result = await sut.getUser('unknown');

      // Assert
      expect(result).toBeNull();
    });
  });
});
typescript
describe('UserService', () => {
  let sut: UserService;
  let mockRepository: jest.Mocked<IUserRepository>;

  beforeEach(() => {
    mockRepository = {
      findById: jest.fn(),
      save: jest.fn(),
    };
    sut = new UserService(mockRepository);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  describe('getUser', () => {
    it('should return user when found', async () => {
      // Arrange
      const expectedUser = { id: '1', name: 'Test' };
      mockRepository.findById.mockResolvedValue(expectedUser);

      // Act
      const result = await sut.getUser('1');

      // Assert
      expect(result).toEqual(expectedUser);
      expect(mockRepository.findById).toHaveBeenCalledWith('1');
    });

    it('should return null when user not found', async () => {
      // Arrange
      mockRepository.findById.mockResolvedValue(null);

      // Act
      const result = await sut.getUser('unknown');

      // Assert
      expect(result).toBeNull();
    });
  });
});

Mocking Strategies

Mock策略

typescript
// Mock modules
jest.mock('./database', () => ({
  getConnection: jest.fn().mockResolvedValue(mockConnection),
}));

// Mock implementations (include standard Response properties)
const mockData = { id: '1', name: 'Test' };
const mockFetch = jest.fn().mockImplementation((url: string) =>
  Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve(mockData) })
);

// Spy on methods
const spy = jest.spyOn(service, 'validate');
expect(spy).toHaveBeenCalledTimes(1);
typescript
// Mock modules
jest.mock('./database', () => ({
  getConnection: jest.fn().mockResolvedValue(mockConnection),
}));

// Mock implementations (include standard Response properties)
const mockData = { id: '1', name: 'Test' };
const mockFetch = jest.fn().mockImplementation((url: string) =>
  Promise.resolve({ ok: true, status: 200, json: () => Promise.resolve(mockData) })
);

// Spy on methods
const spy = jest.spyOn(service, 'validate');
expect(spy).toHaveBeenCalledTimes(1);

Integration Test Patterns

集成测试模式

typescript
describe('API Integration', () => {
  let app: Express;

  beforeAll(async () => {
    app = await createApp({ database: testDb });
  });

  afterAll(async () => {
    await testDb.close();
  });

  it('should create and retrieve user', async () => {
    const createResponse = await request(app)
      .post('/users')
      .send({ name: 'Test', email: 'test@example.com' })
      .expect(201);

    const getResponse = await request(app)
      .get(`/users/${createResponse.body.id}`)
      .expect(200);

    expect(getResponse.body.name).toBe('Test');
  });
});
typescript
describe('API Integration', () => {
  let app: Express;

  beforeAll(async () => {
    app = await createApp({ database: testDb });
  });

  afterAll(async () => {
    await testDb.close();
  });

  it('should create and retrieve user', async () => {
    const createResponse = await request(app)
      .post('/users')
      .send({ name: 'Test', email: 'test@example.com' })
      .expect(201);

    const getResponse = await request(app)
      .get(`/users/${createResponse.body.id}`)
      .expect(200);

    expect(getResponse.body.name).toBe('Test');
  });
});

React Component Testing

React 组件测试

typescript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

describe('Button', () => {
  it('should call onClick when clicked', async () => {
    const user = userEvent.setup();
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click me</Button>);

    await user.click(screen.getByText('Click me'));

    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('should be disabled when loading', () => {
    render(<Button isLoading>Submit</Button>);

    expect(screen.getByRole('button')).toBeDisabled();
  });
});
typescript
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

describe('Button', () => {
  it('should call onClick when clicked', async () => {
    const user = userEvent.setup();
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>Click me</Button>);

    await user.click(screen.getByText('Click me'));

    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('should be disabled when loading', () => {
    render(<Button isLoading>Submit</Button>);

    expect(screen.getByRole('button')).toBeDisabled();
  });
});

Async Testing

异步测试

typescript
// Testing async functions
it('should handle async errors', async () => {
  mockApi.get.mockRejectedValue(new NetworkError('timeout'));

  await expect(service.fetchData('url')).rejects.toThrow(NetworkError);
});

// Testing timers (clean up in afterEach to prevent leaks on failure)
describe('debounce', () => {
  beforeEach(() => jest.useFakeTimers());
  afterEach(() => jest.useRealTimers());

  it('should debounce input', () => {
    const callback = jest.fn();

    debounce(callback, 300)('test');
    expect(callback).not.toHaveBeenCalled();

    jest.advanceTimersByTime(300);
    expect(callback).toHaveBeenCalledWith('test');
  });
});
typescript
// Testing async functions
it('should handle async errors', async () => {
  mockApi.get.mockRejectedValue(new NetworkError('timeout'));

  await expect(service.fetchData('url')).rejects.toThrow(NetworkError);
});

// Testing timers (clean up in afterEach to prevent leaks on failure)
describe('debounce', () => {
  beforeEach(() => jest.useFakeTimers());
  afterEach(() => jest.useRealTimers());

  it('should debounce input', () => {
    const callback = jest.fn();

    debounce(callback, 300)('test');
    expect(callback).not.toHaveBeenCalled();

    jest.advanceTimersByTime(300);
    expect(callback).toHaveBeenCalledWith('test');
  });
});

Vitest Equivalents

Vitest 等价API

If using Vitest instead of Jest, the API is nearly identical:
JestVitest
jest.fn()
vi.fn()
jest.mock()
vi.mock()
jest.spyOn()
vi.spyOn()
jest.clearAllMocks()
vi.clearAllMocks()
jest.useFakeTimers()
vi.useFakeTimers()
jest.Mocked<T>
Mocked<T>
(from
vitest
)
如果使用Vitest替代Jest,API几乎完全一致:
JestVitest
jest.fn()
vi.fn()
jest.mock()
vi.mock()
jest.spyOn()
vi.spyOn()
jest.clearAllMocks()
vi.clearAllMocks()
jest.useFakeTimers()
vi.useFakeTimers()
jest.Mocked<T>
Mocked<T>
(from
vitest
)

Coverage Guidelines

覆盖率指南

  • Enforce ≥80% coverage on critical business logic
  • Don't chase 100% - focus on meaningful tests
  • Never commit real
    .env
    files or API keys in tests
  • Use test fixtures for complex data structures
  • 强制要求核心业务逻辑的覆盖率≥80%
  • 不要盲目追求100%覆盖率——重点放在有实际意义的测试上
  • 绝对不要在测试中提交真实的
    .env
    文件或API密钥
  • 针对复杂数据结构使用测试夹具