正在加载,请稍候…

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

Write effective Jest tests in TypeScript. Learn unit testing patterns, mocking modules and functions, testing async code, snapshot testing, and achieving meaningful coverage.

Jest Testing in TypeScript

Setup

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

Basic Tests

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

Mocking

// Auto mock module
jest.mock('./emailService');
import { sendEmail } from './emailService';
const mockSendEmail = sendEmail as jest.MockedFunction<typeof sendEmail>;

// Manual mock
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);

    // Reset mocks before each test
    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();
  });
});

Async Testing

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

  // Testing with fake timers
  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);  // Advance past retry delay
    const user = await promise;

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

Custom Matchers

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

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

Good tests document expected behavior and enable fearless refactoring.