How to Mock Functions in TypeScript: Complete Guide with Examples - comprehensive 2026 data and analysis

How to Mock Functions in TypeScript: Complete Guide with Examples

Last verified: April 2026

Executive Summary

Mocking functions is one of the most critical skills for writing reliable unit tests in TypeScript. Whether you’re using Jest (the dominant choice among TypeScript developers), Vitest, or Sinon, the underlying principles remain consistent: replace real function implementations with controlled test doubles that let you verify behavior without side effects. Our analysis shows that intermediate-level developers encounter mocking challenges most frequently when dealing with async functions, class methods, and module-level imports—three scenarios we’ll cover in depth.

Learn TypeScript on Udemy


View on Udemy →

This guide walks you through the mechanics of function mocking, from basic spy setup to advanced patterns like partial mocking and return value assertion. You’ll learn the common pitfalls that trip up even experienced developers—particularly around not properly handling edge cases, ignoring error scenarios, and forgetting to reset mocks between tests. We’ve structured this as a practical tutorial with production-ready code examples and clear explanations of why each approach matters for your test suite.

Main Data Table

Mocking Approach Primary Use Case Setup Complexity Best For
Jest Mock Functions Unit testing isolated functions Low Single function mocking, return values
Jest Module Mocking Replacing entire module imports Medium External API calls, file system operations
Spy Functions Monitoring real function calls Medium Verifying call count, arguments passed
Sinon Stubs Complex behavior replacement High Advanced scenarios, multiple calls with different returns
Vitest Mocking Fast unit testing with ESM support Low-Medium Modern TypeScript projects with ES modules

Breakdown by Experience Level

Mocking complexity varies significantly based on what you’re testing. Here’s how difficulty scales:

Experience Level Common Task Typical Challenge Recommended Tool
Beginner Mock simple synchronous function Understanding jest.fn() basics Jest
Intermediate Mock async functions and class methods Handling promises, class inheritance Jest with spyOn
Advanced Partial mocking, conditional returns Edge cases, state management across tests Jest + Sinon or Vitest

Getting Started: Basic Function Mocking with Jest

The simplest mocking approach uses Jest’s jest.fn(), which creates a mock function that tracks calls and return values:

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

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

test('mock add function', () => {
  const mockAdd = jest.fn((a: number, b: number) => a + b);
  
  mockAdd(2, 3);
  
  expect(mockAdd).toHaveBeenCalledWith(2, 3);
  expect(mockAdd).toHaveBeenCalledTimes(1);
  expect(mockAdd).toHaveReturnedWith(5);
});

This pattern creates a mock that behaves like the real function but also records every interaction. Notice that we’re testing how the function was called, not just what it returns—this is the essence of mocking.

Mocking Async Functions and Promises

Async functions require special handling because they return Promises. This is where many developers stumble:

// api.ts
export async function fetchUser(id: number): Promise<{name: string}> {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

// api.test.ts
import { fetchUser } from './api';

test('mock async fetch function', async () => {
  const mockFetchUser = jest.fn().mockResolvedValue({ name: 'John' });
  
  const result = await mockFetchUser(1);
  
  expect(mockFetchUser).toHaveBeenCalledWith(1);
  expect(result).toEqual({ name: 'John' });
});

// Handle rejection
test('mock async function that rejects', async () => {
  const mockFetchUser = jest.fn().mockRejectedValue(new Error('Network error'));
  
  await expect(mockFetchUser(1)).rejects.toThrow('Network error');
});

mockResolvedValue and mockRejectedValue handle the Promise wrapper automatically—a critical detail that separates working async tests from flaky ones.

Spying on Real Methods with jest.spyOn

Sometimes you want to monitor an existing function without replacing it entirely. That’s where spies come in:

// userService.ts
export class UserService {
  getUser(id: number) {
    return { id, name: 'Alice' };
  }
}

// userService.test.ts
import { UserService } from './userService';

test('spy on class method', () => {
  const service = new UserService();
  const spyGetUser = jest.spyOn(service, 'getUser');
  
  const result = service.getUser(1);
  
  expect(spyGetUser).toHaveBeenCalledWith(1);
  expect(result).toEqual({ id: 1, name: 'Alice' });
  
  spyGetUser.mockReturnValue({ id: 2, name: 'Bob' });
  const newResult = service.getUser(1);
  expect(newResult).toEqual({ id: 2, name: 'Bob' });
  
  spyGetUser.mockRestore();
});

The key difference: a spy wraps the real method, letting you verify it was called while optionally changing its behavior. Always call mockRestore() to clean up—or use jest.spyOn().mockRestore() in afterEach blocks to prevent test pollution.

Mocking Module Imports

External dependencies like APIs or databases need module-level mocking:

// database.ts
export async function queryDatabase(sql: string) {
  // Real database call
  return { rows: [] };
}

// userRepository.ts
import { queryDatabase } from './database';

export async function getUserById(id: number) {
  const result = await queryDatabase(`SELECT * FROM users WHERE id = ${id}`);
  return result.rows[0];
}

// userRepository.test.ts
import { getUserById } from './userRepository';
import * as database from './database';

jest.mock('./database');

test('mock module import', async () => {
  const mockQueryDatabase = database.queryDatabase as jest.MockedFunction<typeof database.queryDatabase>;
  mockQueryDatabase.mockResolvedValue({
    rows: [{ id: 1, name: 'Alice' }]
  });
  
  const user = await getUserById(1);
  
  expect(mockQueryDatabase).toHaveBeenCalledWith(
    expect.stringContaining('SELECT')
  );
  expect(user).toEqual({ id: 1, name: 'Alice' });
});

The jest.mock() call at the top intercepts all imports of that module. This is powerful for isolating units under test from external systems, but watch for accidentally mocking too much—always mock at the boundary between your code and external dependencies.

Comparison with Alternative Approaches

Technique Jest Mock Sinon Stub Vitest Manual Test Double
Setup Time Fast Moderate Fast Slow
TypeScript Support Excellent Good Native ESM N/A
Module Mocking Yes Limited Yes No
Learning Curve Gentle Steep Gentle Very Gentle
Typical Use Case Unit tests Complex stubs Modern projects Simple cases

Key Factors to Consider

1. Edge Cases and Null Value Handling

The most common mistake in mocking is ignoring edge cases. Your mocks should handle empty inputs, null values, and boundary conditions just like your real code:

test('mock function handles null input', () => {
  const mockProcess = jest.fn((data: string | null) => {
    if (!data) return { error: 'No data' };
    return { success: true, length: data.length };
  });
  
  expect(mockProcess(null)).toEqual({ error: 'No data' });
  expect(mockProcess('')).toEqual({ success: true, length: 0 });
  expect(mockProcess('test')).toEqual({ success: true, length: 4 });
});

2. Error Handling with Try/Catch

Always test error paths. Wrapping mocks in try/catch ensures your code handles failures gracefully:

test('mock function error handling', async () => {
  const mockApiCall = jest.fn().mockRejectedValue(new Error('Timeout'));
  
  try {
    await mockApiCall();
  } catch (error) {
    expect(error).toBeInstanceOf(Error);
    expect((error as Error).message).toBe('Timeout');
  }
});

3. Reset Mocks Between Tests

Forgetting to reset mocks causes test pollution. Use clearMocks() or resetAllMocks() in afterEach:

describe('mock cleanup', () => {
  const mockFn = jest.fn();
  
  afterEach(() => {
    mockFn.mockClear(); // Clears call history but keeps implementation
  });
  
  test('first test', () => {
    mockFn('first');
    expect(mockFn).toHaveBeenCalledTimes(1);
  });
  
  test('second test', () => {
    mockFn('second');
    expect(mockFn).toHaveBeenCalledTimes(1); // Would fail without mockClear
  });
});

4. Resource Cleanup with Finally Blocks

If your mock manages resources (files, connections), use finally blocks:

test('mock file operation with cleanup', () => {
  const mockFileOp = jest.fn();
  const mockClose = jest.fn();
  
  try {
    mockFileOp();
    expect(mockFileOp).toHaveBeenCalled();
  } finally {
    mockClose();
    expect(mockClose).toHaveBeenCalled();
  }
});

5. Performance-Conscious Mocking Strategies

Inefficient mocks slow down test suites. Use partial mocks sparingly and avoid mocking more than necessary:

// Instead of mocking entire modules, mock specific functions
const mockExpensive = jest.fn().mockResolvedValue({ cached: true });

// This runs once, not for every test
beforeAll(() => {
  mockExpensive.mockImplementation((x) => Promise.resolve(x * 2));
});

Historical Trends and Evolution

Mocking practices in TypeScript have evolved significantly since 2020. Jest dominated early adoption with simple jest.fn() APIs. By 2023-2024, Vitest emerged as a faster alternative with native ESM support—critical for projects using TypeScript’s modern module system. Sinon remains relevant for complex scenarios but has lost market share to Jest and Vitest for standard unit testing. In 2026, the consensus is clear: Jest for established projects, Vitest for greenfield TypeScript work.

One interesting shift: mocking has moved upstream. Modern frameworks like Next.js and SvelteKit now handle mocking automatically through built-in test utilities. However, understanding low-level mocking mechanics remains essential for any developer working with TypeScript.

Expert Tips

Tip 1: Use Type-Safe Mocks with MockedFunction

Always cast mocks to proper types to maintain TypeScript’s type safety:

import { jest } from '@jest/globals';
import type { UserService } from './userService';

const mockService = jest.fn<typeof UserService.prototype.getUser>();
// Now mockService has proper type information

Tip 2: Implement Partial Mocking for Complex Objects

For large objects, only mock the parts you need to test:

const mockConfig = {
  database: {
    host: 'localhost',
    port: 5432,
    query: jest.fn().mockResolvedValue([]) // Only mock the function you use
  }
};

Tip 3: Use Mock Factories for Reusable Patterns

Create helper functions to reduce boilerplate:

// test-helpers.ts
export function createMockUserService() {
  return {
    getUser: jest.fn().mockResolvedValue({ id: 1, name: 'Test' }),
    saveUser: jest.fn().mockResolvedValue({ success: true })
  };
}

// In tests:
const mockService = createMockUserService();

Tip 4: Verify Mock Implementation Matches Reality

A mock that doesn’t behave like the real function defeats testing. Periodically verify mocks against actual implementations or use snapshot testing for complex objects.

Tip 5: Avoid Over-Mocking

Mock only external dependencies and slow operations. Testing real business logic through mocked dependencies defeats the purpose of unit testing.

FAQ Section

What’s the difference between jest.fn() and jest.spyOn()?

jest.fn() creates a completely new mock function from scratch, replacing the original entirely. jest.spyOn() wraps an existing method, allowing you to monitor its calls while still executing the real implementation (unless you override it). Use jest.fn() for mocking standalone functions or when you want complete control over behavior. Use jest.spyOn() when you want to verify that a real method was called or partially modify its behavior.

How do I mock a function that returns different values on different calls?

Use mockReturnValueOnce() or mockImplementationOnce() for sequential returns, or chain them together:

const mockFn = jest.fn()
  .mockReturnValueOnce('first')
  .mockReturnValueOnce('second')
  .mockReturnValue('default');

mockFn(); // returns 'first'
mockFn(); // returns 'second'
mockFn(); // returns 'default'
mockFn(); // returns 'default'

Should I mock TypeScript interfaces or only implementations?

Mock implementations, not interfaces. Interfaces are compile-time only and don’t exist at runtime. Create mock objects that satisfy the interface contract: const mockUser: User = { id: 1, name: 'Test' }. If you’re mocking methods on classes, use jest.spyOn() to target the actual runtime implementation.

How do I prevent test pollution from unmocked modules?

Use jest.resetModules() before each test if you need to reimport modules with different mock states. Better yet, use beforeEach() and afterEach() hooks to clear mocks:

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

What’s the best practice for mocking timers in TypeScript?

Use jest.useFakeTimers() to control setTimeout and setInterval, then advance time with jest.runAllTimers() or jest.advanceTimersByTime():

test('mock timers', () => {
  jest.useFakeTimers();
  const callback = jest.fn();
  
  setTimeout(callback, 1000);
  jest.advanceTimersByTime(1000);
  
  expect(callback).toHaveBeenCalledTimes(1);
  jest.useRealTimers();
});

Conclusion

Mocking functions in TypeScript isn’t about replacing code carelessly—it’s about creating controlled test environments where you verify behavior with precision. Start with jest.fn() for simple cases, graduate to jest.spyOn() for monitoring real methods, and only reach for advanced tools like Sinon when you genuinely need them. Always handle edge cases, always reset your mocks between tests, and always remember that a mock should behave like the real thing, only with extra observability.

The four critical things to remember: test edge cases (empty inputs, null values, boundaries), wrap operations in try/catch blocks, reset mocks in afterEach hooks, and close resources in finally blocks. Follow these principles and your test suite will be both reliable and maintainable. For the most current APIs and patterns, refer to the official Jest or Vitest documentation—testing tooling evolves quickly, and staying current matters.

Learn TypeScript on Udemy

Related: How to Create Event Loop in Python: Complete Guide with Exam


Related tool: Try our free calculator

Similar Posts