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
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