Skip to content
intermediate Phase 7 · TypeScript Testing & Tools

Jest with TypeScript

Write and run TypeScript tests with Jest and ts-jest configuration.

1h
0 problems
Topic Progress 0%

Jest Setup

Jest Setup with TypeScript

Installation

npm install --save-dev jest @types/jest ts-jest

jest.config.ts

import type { Config } from 'jest';

const config: Config = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/*.test.ts', '**/*.spec.ts'],
  moduleFileExtensions: ['ts', 'js', 'json'],
  collectCoverage: true,
  coverageDirectory: 'coverage',
  coverageReporters: ['text', 'lcov'],
  clearMocks: true,
  restoreMocks: true
};

export default config;

Basic Test

// math.ts
export function add(a: number, b: number): number {
  return a + b;
}

// math.test.ts
import { add } from './math';

describe('add', () => {
  it('should add two positive numbers', () => {
    expect(add(2, 3)).toBe(5);
  });

  it('should handle negative numbers', () => {
    expect(add(-1, -2)).toBe(-3);
  });

  it('should handle zero', () => {
    expect(add(0, 5)).toBe(5);
  });
});

Type-Safe Assertions

Type-Safe Assertions

Common Matchers

interface User {
  id: number;
  name: string;
  email: string;
  age?: number;
}

function createUser(name: string, email: string): User {
  return { id: Date.now(), name, email };
}

describe('createUser', () => {
  it('should create user with required fields', () => {
    const user = createUser('Alice', 'alice@test.com');

    expect(user).toEqual({
      id: expect.any(Number),
      name: 'Alice',
      email: 'alice@test.com'
    });
  });

  it('should have correct types', () => {
    const user = createUser('Alice', 'alice@test.com');
    expect(typeof user.id).toBe('number');
    expect(typeof user.name).toBe('string');
  });

  it('should match object shape', () => {
    const user = createUser('Alice', 'alice@test.com');
    expect(user).toMatchObject({
      name: expect.stringContaining('Ali'),
      email: expect.stringMatching(/@test\.com$/)
    });
  });
});

Type Guards in Tests

type Result<T> = { success: true; data: T } | { success: false; error: string };

function isSuccess<T>(result: Result<T>): result is { success: true; data: T } {
  return result.success;
}

describe('Result type', () => {
  it('should narrow success result', () => {
    const result: Result<number> = { success: true, data: 42 };
    expect(isSuccess(result)).toBe(true);
    if (isSuccess(result)) {
      expect(result.data).toBe(42);
    }
  });
});

Mocking with TypeScript

Mocking with TypeScript

Mock Functions

type FetchFn = (url: string) => Promise<Response>;

function processUser(fetch: FetchFn, id: string): Promise<string> {
  return fetch(`/api/users/${id}`)
    .then(res => res.json())
    .then(data => data.name);
}

describe('processUser', () => {
  it('should fetch and process user name', async () => {
    const mockFetch = jest.fn<FetchFn>().mockResolvedValue({
      json: () => Promise.resolve({ name: 'Alice' })
    } as Response);

    const name = await processUser(mockFetch, '123');
    expect(name).toBe('Alice');
    expect(mockFetch).toHaveBeenCalledWith('/api/users/123');
  });
});

Module Mocking

// UserService uses Database
jest.mock('./database.js');
import { db } from './database.js';

const mockDb = jest.mocked(db);

describe('UserService', () => {
  it('should find user by id', async () => {
    mockDb.query.mockResolvedValueOnce([{ id: '1', name: 'Alice' }]);

    const user = await userService.findById('1');
    expect(user).toEqual({ id: '1', name: 'Alice' });
    expect(mockDb.query).toHaveBeenCalledWith(
      'SELECT * FROM users WHERE id = ?',
      ['1']
    );
  });
});

Spy on Methods

class Logger {
  log(msg: string) { console.log(msg); }
  error(msg: string) { console.error(msg); }
}

describe('Logger', () => {
  it('should log messages', () => {
    const logger = new Logger();
    const spy = jest.spyOn(logger, 'log');

    logger.log('hello');
    expect(spy).toHaveBeenCalledWith('hello');
    expect(spy).toHaveBeenCalledTimes(1);
  });
});

Async Testing

Async Testing

Testing Promises

function fetchData(url: string): Promise<{ data: string }> {
  return fetch(url).then(res => res.json());
}

describe('fetchData', () => {
  it('should return data on success', async () => {
    global.fetch = jest.fn().mockResolvedValue({
      json: () => Promise.resolve({ data: 'hello' })
    });

    const result = await fetchData('/api');
    expect(result).toEqual({ data: 'hello' });
  });

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

    await expect(fetchData('/api')).rejects.toThrow('Network error');
  });
});

Testing with beforeEach/afterEach

describe('UserService', () => {
  let service: UserService;

  beforeEach(() => {
    service = new UserService();
    jest.clearAllMocks();
  });

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

  it('should create user', async () => {
    const user = await service.create({ name: 'Alice' });
    expect(user.id).toBeDefined();
    expect(user.name).toBe('Alice');
  });
});

Coverage Configuration

// package.json
{
  "jest": {
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,
        "statements": 80
      }
    }
  }
}

Best Practices

  • Use jest.fn<T>() for typed mock functions
  • Use jest.mocked() to type mocked modules
  • Test behavior, not implementation details
  • Keep tests isolated with proper setup/teardown
  • Use describe blocks to organize related tests