Skip to main content

Mocking & Spies

Real applications depend on databases, APIs, file systems, and other external services. You cannot (and should not) call these in unit tests. Mocking replaces real dependencies with controlled substitutes so you can test your code in isolation.

jest.fn() — Creating Mock Functions

jest.fn() creates a function that records how it was called:

const mockCallback = jest.fn();

// Call it
mockCallback('hello');
mockCallback('world');

// Assert how it was called
expect(mockCallback).toHaveBeenCalledTimes(2);
expect(mockCallback).toHaveBeenCalledWith('hello');
expect(mockCallback).toHaveBeenLastCalledWith('world');

You can also make mock functions return values:

const getPrice = jest.fn()
  .mockReturnValue(9.99)               // default return
  .mockReturnValueOnce(19.99)          // first call returns this
  .mockReturnValueOnce(29.99);         // second call returns this

console.log(getPrice()); // 19.99 (first call)
console.log(getPrice()); // 29.99 (second call)
console.log(getPrice()); // 9.99  (default after that)

For async functions, use mockResolvedValue:

const fetchUser = jest.fn().mockResolvedValue({
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
});

const user = await fetchUser(1);
expect(user.name).toBe('Alice');

jest.mock() — Mocking Entire Modules

When your code imports a module, jest.mock() replaces the entire module with mock functions:

// userService.ts
import { db } from './database';

export async function getUser(id: number) {
  const row = await db.query('SELECT * FROM users WHERE id = $1', [id]);
  return row;
}
// userService.test.ts
import { getUser } from './userService';
import { db } from './database';

jest.mock('./database');

const mockDb = db as jest.Mocked<typeof db>;

test('getUser queries the database by ID', async () => {
  mockDb.query.mockResolvedValue({ id: 1, name: 'Alice' });

  const user = await getUser(1);

  expect(mockDb.query).toHaveBeenCalledWith(
    'SELECT * FROM users WHERE id = $1',
    [1]
  );
  expect(user.name).toBe('Alice');
});

jest.mock('./database') automatically replaces every export from the database module with jest.fn(). You then configure specific return values in each test.

jest.spyOn() — Watching Real Functions

spyOn wraps an existing function so you can track calls while keeping the original behavior:

const mathService = {
  add(a: number, b: number) {
    return a + b;
  },
};

test('tracks calls to add', () => {
  const spy = jest.spyOn(mathService, 'add');

  const result = mathService.add(2, 3);

  expect(spy).toHaveBeenCalledWith(2, 3);
  expect(result).toBe(5); // real implementation runs
  spy.mockRestore();
});

You can also override the implementation:

const spy = jest.spyOn(mathService, 'add').mockReturnValue(42);

expect(mathService.add(1, 1)).toBe(42); // mock overrides
spy.mockRestore();
expect(mathService.add(1, 1)).toBe(2);  // real behavior restored

Mocking API Calls

Testing code that makes HTTP requests is a common scenario. Here is how to mock fetch:

// api.ts
export async function fetchTodos() {
  const response = await fetch('https://api.example.com/todos');
  if (!response.ok) throw new Error('Failed to fetch');
  return response.json();
}
// api.test.ts
import { fetchTodos } from './api';

beforeEach(() => {
  global.fetch = jest.fn();
});

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

test('returns todos on success', async () => {
  const mockTodos = [{ id: 1, title: 'Write tests' }];

  (fetch as jest.Mock).mockResolvedValue({
    ok: true,
    json: async () => mockTodos,
  });

  const todos = await fetchTodos();
  expect(todos).toEqual(mockTodos);
  expect(fetch).toHaveBeenCalledWith('https://api.example.com/todos');
});

test('throws on network failure', async () => {
  (fetch as jest.Mock).mockResolvedValue({ ok: false });

  await expect(fetchTodos()).rejects.toThrow('Failed to fetch');
});

When to Mock vs When Not To

Mocking is powerful but overusing it creates brittle tests that test implementation details rather than behavior.

Do mock:

  • External API calls (network requests)
  • Database queries
  • File system operations
  • Timers and dates (jest.useFakeTimers())
  • Third-party services (email, payment)

Do not mock:

  • Pure utility functions — test them with real inputs and outputs.
  • The module you are testing — you should test real behavior.
  • Everything — if you mock every dependency, your test proves nothing about how the system actually works.

A good rule of thumb: mock at the boundary of your system. Mock the HTTP client, not every internal function that passes data around.

Key Takeaways

  • jest.fn() creates trackable mock functions with configurable return values.
  • jest.mock() replaces entire modules — use it for database, API, and file system dependencies.
  • jest.spyOn() tracks calls while keeping real behavior by default.
  • Mock at system boundaries (network, database, file system), not internal functions.
  • Always call jest.restoreAllMocks() in afterEach to prevent test pollution.