Jest’s mocking utilities are powerful, but the distinction between fakes, stubs, and spies can be fuzzy, leading to misuse.

Let’s see how this looks in practice. Imagine we have a userService that fetches user data and a postService that creates posts. We want to test postService without actually hitting a network or database.

// userService.js
export const fetchUser = async (userId) => {
  const response = await fetch(`/api/users/${userId}`);
  return response.json();
};

// postService.js
import { fetchUser } from './userService';

export const createPost = async (userId, postContent) => {
  const user = await fetchUser(userId);
  if (!user) {
    throw new Error('User not found');
  }
  // ... actual post creation logic ...
  console.log(`Creating post for ${user.name}: ${postContent}`);
  return { userId, postContent, timestamp: Date.now() };
};

Now, we want to test createPost.

Fakes: A Simpler, Working Replacement

A fake replaces a complex dependency with a simplified, working version. It’s not just about preventing side effects; it’s about providing a functional, albeit less robust, alternative.

// __mocks__/userService.js (Jest mock factory)
export const fetchUser = jest.fn(async (userId) => {
  // A simplified, hardcoded user for testing
  if (userId === 123) {
    return { id: 123, name: 'Alice' };
  }
  return null; // Simulate user not found
});

In our test:

// postService.test.js
import { createPost } from '../postService';
import * as userService from '../userService'; // Import the module to mock

// Jest will automatically use the __mocks__/userService.js
// if it exists and we import userService.

describe('createPost with Fake', () => {
  it('should create a post for a known user', async () => {
    const userId = 123;
    const postContent = 'Hello, world!';
    const result = await createPost(userId, postContent);

    expect(result.userId).toBe(userId);
    expect(result.postContent).toBe(postContent);
    expect(userService.fetchUser).toHaveBeenCalledWith(userId); // Verify the fake was called
  });

  it('should throw an error if user is not found', async () => {
    const userId = 456; // This user isn't in our fake
    const postContent = 'Another post';

    await expect(createPost(userId, postContent)).rejects.toThrow('User not found');
    expect(userService.fetchUser).toHaveBeenCalledWith(userId);
  });
});

Why it works: The __mocks__/userService.js provides a concrete, albeit simple, implementation of fetchUser. Jest intercepts calls to userService.fetchUser and uses our fake instead. This fake works by returning specific data, allowing createPost to execute its logic as if it got real data.

Stubs: Pre-programmed Responses

A stub is similar to a fake but is specifically designed to provide canned answers to calls made during a test. It doesn’t necessarily implement the full logic of the original dependency; it just returns what you tell it to.

// postService.test.js (continued)
import { createPost } from '../postService';
import { fetchUser } from '../userService'; // Import the function directly

// Clear mocks before each test to ensure isolation
beforeEach(() => {
  fetchUser.mockClear();
});

describe('createPost with Stub', () => {
  it('should create a post with stubbed user data', async () => {
    const userId = 789;
    const postContent = 'Stubbed post';

    // Configure the stub to return specific data
    fetchUser.mockResolvedValue({ id: userId, name: 'Bob' });

    const result = await createPost(userId, postContent);

    expect(result.userId).toBe(userId);
    expect(result.postContent).toBe(postContent);
    expect(fetchUser).toHaveBeenCalledTimes(1);
    expect(fetchUser).toHaveBeenCalledWith(userId);
  });

  it('should handle stubbed user not found', async () => {
    const userId = 101;
    const postContent = 'Post for non-existent user';

    // Configure the stub to return null (or undefined)
    fetchUser.mockResolvedValue(null);

    await expect(createPost(userId, postContent)).rejects.toThrow('User not found');
    expect(fetchUser).toHaveBeenCalledTimes(1);
    expect(fetchUser).toHaveBeenCalledWith(userId);
  });
});

Why it works: jest.fn().mockResolvedValue() directly intercepts the fetchUser function and dictates its return value for that specific test. Unlike a fake, we’re not providing a mini-implementation of fetchUser; we’re just telling it what to return when called, allowing createPost to proceed with predictable data.

Spies: Observing Behavior Without Altering It

A spy wraps an existing function to observe its behavior (how many times it was called, with what arguments) without changing its implementation. You can then assert on these observations. Spies are often used when you don’t want to mock the behavior, but just confirm it happened.

// postService.test.js (continued)
import { createPost } from '../postService';
import * as userService from '../userService'; // Import the module

describe('createPost with Spy', () => {
  it('should call fetchUser with the correct userId', async () => {
    // Spy on the fetchUser function within the userService module
    const fetchUserSpy = jest.spyOn(userService, 'fetchUser');

    const userId = 246;
    const postContent = 'Spying on fetch';

    // For this test, we *want* fetchUser to actually run (or be faked by a top-level mock)
    // If we had a __mocks__/userService.js, Jest would use that.
    // For a true spy test, we often ensure the actual dependency is available or a global mock is set.
    // Let's assume for this example, a global mock is NOT active, and fetchUser would hit a real API (bad for testing).
    // A more practical spy test would ensure the underlying dependency is *also* stubbed or faked.
    // For demonstration, let's stub it *and* spy on it.

    fetchUserSpy.mockResolvedValue({ id: userId, name: 'Charlie' }); // Stubbing the actual function

    await createPost(userId, postContent);

    // Assertions on the spy
    expect(fetchUserSpy).toHaveBeenCalledTimes(1);
    expect(fetchUserSpy).toHaveBeenCalledWith(userId);

    // Clean up the spy after the test
    fetchUserSpy.mockRestore();
  });

  it('should ensure fetchUser is called when user is not found', async () => {
    const fetchUserSpy = jest.spyOn(userService, 'fetchUser');
    const userId = 808;
    const postContent = 'Post that should fail';

    fetchUserSpy.mockResolvedValue(null); // Stubbing the actual function

    await expect(createPost(userId, postContent)).rejects.toThrow('User not found');

    expect(fetchUserSpy).toHaveBeenCalledTimes(1);
    expect(fetchUserSpy).toHaveBeenCalledWith(userId);

    fetchUserSpy.mockRestore();
  });
});

Why it works: jest.spyOn(userService, 'fetchUser') creates a wrapper around the original fetchUser. We can then use .mockResolvedValue() on this spy to provide a stubbed response while simultaneously recording that fetchUser was called, how many times, and with what arguments. .mockRestore() is crucial to remove the spy and revert the original function after the test.

When to Use What: The Nuance

  • Fakes are best when you need a functional, simpler version of a dependency that still behaves like the real thing, allowing complex logic within your system under test to execute naturally. Think of replacing a database with an in-memory array.
  • Stubs are your go-to when you need to control the exact data your function receives, ensuring specific code paths are hit, or when you want to simulate error conditions predictably. This is the most common form of mocking for unit tests.
  • Spies are for observing whether a dependency was interacted with, and how, without necessarily changing its behavior (though often combined with stubs). Use them when you care more about the interaction than the outcome.

The key difference is intent: Fakes provide a simplified implementation; Stubs provide pre-defined return values; Spies observe existing behavior.

The next error you’ll hit is likely a ReferenceError if you forget to mockRestore() your spies in a test suite that reuses modules, or if you try to stub a function that doesn’t exist.

Want structured learning?

Take the full Jest course →