Mocking and Spying in JavaScript Testing

In JavaScript testing, writing reliable unit tests often requires isolating code from external dependencies, complex subsystems, or side effects like network requests. This article provides a comprehensive overview of mocking and spying in modern JavaScript test runners such as Jest, Vitest, and Jasmine. It covers what these test doubles are, how they differ, and when to apply each technique to verify function behavior and state interactions cleanly and efficiently.

What is a Spy?

A spy is a test double that observes and records how a function is called without replacing its underlying implementation by default. When you spy on a method, the test runner wraps the existing function and tracks its execution metadata.

What Spies Record:

Common Use Cases for Spies:

In frameworks like Jest and Vitest, you typically create a spy using syntax such as jest.spyOn(object, 'methodName') or vi.spyOn(object, 'methodName').


What is a Mock?

A mock is a test double that completely replaces a real function, object, or module with a simulated version. Unlike a spy, a mock typically provides a pre-programmed response and avoids executing the real implementation entirely.

Core Characteristics of Mocks:

Common Use Cases for Mocks:

In modern test runners, mocks are declared via methods like jest.fn(), jest.mock('module-name'), or vi.mock('module-name').


Key Differences: Mocks vs. Spies

Feature Spy Mock
Primary Purpose Observation and verification Replacement and control
Original Implementation Preserved and executed by default Replaced with empty or custom logic
Side Effects Real side effects still occur unless overridden Real side effects are completely bypassed
Best Used For Verifying method invocations and arguments Simulating external APIs, databases, or heavy modules

When to Use Which Technique

Use a Spy When:

  1. You want to test that a method on an object is triggered, but the execution of that method is harmless and deterministic.
  2. You want to temporarily track a method during a specific test block and then restore the original behavior using mockRestore().

Use a Mock When:

  1. The dependency involves I/O operations, network traffic, or state mutations outside the current scope.
  2. The real implementation is slow or non-deterministic (e.g., timers, random number generators).
  3. You need to simulate specific error conditions (like a 500 server response) that are difficult to trigger against real services.

Best Practices for Using Spies and Mocks