正在加载,请稍候…

TypeScript 中的 Jest 测试:模拟、异步测试与覆盖率

学习在 TypeScript 中编写高效的 Jest 测试,涵盖单元测试模式、模拟模块和函数、异步代码测试、快照测试以及实现有意义的覆盖率。

Jest Testing in TypeScript: Mocking, Async Tests, and Coverage

TypeScript 中的 Jest 测试

设置

npm install -D jest @types/jest ts-jest
// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/*.test.ts', '**/*.spec.ts'],
  collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts', '!src/**/__tests__/**'],
  coverageThresholds: {
    global: { lines: 80, functions: 80, branches: 70 },
  },
};

Jest Testing in TypeScript: Mocking, Async Tests, and Coverage illustration

基本测试

import { calculateDiscount, formatPrice } from './pricing';

describe('calculateDiscount', () => {
  it('should apply percentage discount', () => {
    expect(calculateDiscount(100, 10)).toBe(90);
    expect(calculateDiscount(50, 25)).toBe(37.5);
  });

  it('should return full price when discount is 0', () => {
    expect(calculateDiscount(100, 0)).toBe(100);
  });

  it('should throw for invalid discount', () => {
    expect(() => calculateDiscount(100, 110)).toThrow('Discount cannot exceed 100%');
    expect(() => calculateDiscount(100, -5)).toThrow('Discount cannot be negative');
  });

  it.each([
    [100, 10, 90],
    [200, 50, 100],
    [75, 20, 60],
  ])('calculateDiscount(%d, %d) = %d', (price, discount, expected) => {
    expect(calculateDiscount(price, discount)).toBe(expected);
  });
});

Jest Testing in TypeScript: Mocking, Async Tests, and Coverage illustration

模拟(Mocking)

// 自动模拟模块
jest.mock('./emailService');
import { sendEmail } from './emailService';
const mockSendEmail = sendEmail as jest.MockedFunction<typeof sendEmail>;

// 手动模拟
jest.mock('./database', () => ({
  query: jest.fn(),
  insert: jest.fn(),
  close: jest.fn(),
}));

describe('UserService', () => {
  let userService: UserService;
  let mockUserRepo: jest.Mocked<UserRepository>;

  beforeEach(() => {
    mockUserRepo = {
      findById: jest.fn(),
      save: jest.fn(),
      delete: jest.fn(),
    };
    userService = new UserService(mockUserRepo, mockSendEmail);

    // 在每个测试前重置模拟
    jest.clearAllMocks();
  });

  it('should send welcome email on registration', async () => {
    const savedUser = { id: '1', email: 'alice@example.com', name: 'Alice' };
    mockUserRepo.save.mockResolvedValue(savedUser);
    mockSendEmail.mockResolvedValue(undefined);

    await userService.register({ email: 'alice@example.com', name: 'Alice' });

    expect(mockSendEmail).toHaveBeenCalledWith({
      to: 'alice@example.com',
      subject: 'Welcome!',
      body: expect.stringContaining('Alice'),
    });
  });

  it('should not send email if save fails', async () => {
    mockUserRepo.save.mockRejectedValue(new Error('DB error'));

    await expect(userService.register({ email: 'alice@example.com', name: 'Alice' }))
      .rejects.toThrow('DB error');

    expect(mockSendEmail).not.toHaveBeenCalled();
  });
});

Jest Testing in TypeScript: Mocking, Async Tests, and Coverage illustration

异步测试

describe('fetchUser', () => {
  it('returns user on success', async () => {
    global.fetch = jest.fn().mockResolvedValue({
      ok: true,
      json: jest.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
    });

    const user = await fetchUser('1');
    expect(user).toEqual({ id: '1', name: 'Alice' });
  });

  it('throws on network error', async () => {
    global.fetch = jest.fn().mockRejectedValue(new Error('Network Error'));

    await expect(fetchUser('1')).rejects.toThrow('Network Error');
  });

  // 使用假定时器测试
  it('retries after delay', async () => {
    jest.useFakeTimers();
    const mockFetch = jest.fn()
      .mockRejectedValueOnce(new Error('timeout'))
      .mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ id: '1' }) });
    global.fetch = mockFetch;

    const promise = fetchUserWithRetry('1');
    jest.advanceTimersByTime(1000);  // 前进超过重试延迟
    const user = await promise;

    expect(mockFetch).toHaveBeenCalledTimes(2);
    jest.useRealTimers();
  });
});

自定义匹配器

expect.extend({
  toBeValidEmail(received: string) {
    const pass = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(received);
    return {
      pass,
      message: () => `Expected ${received} to be a valid email`,
    };
  },
});

// 使用
expect('alice@example.com').toBeValidEmail();

好的测试记录了预期行为,并支持无畏的重构。