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:
- How many times the function was called.
- The arguments passed to each function invocation.
- What values the function returned.
- What errors or exceptions were thrown.
Common Use Cases for Spies:
- Verifying that an event listener or callback was executed.
- Ensuring a method inside an existing utility module was called with specific parameters.
- Validating execution flow without disrupting the actual business logic of the method.
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:
- Implementation Replacement: The original code inside the function does not run, preventing unintended side effects.
- Configurable Outputs: You can configure mock return values, resolved/rejected promises, or custom substitute functions.
- Isolation: It decouples unit tests from slow or non-deterministic dependencies like databases, file systems, and external HTTP APIs.
Common Use Cases for Mocks:
- Simulating API calls with hardcoded JSON responses instead of sending actual network requests.
- Simulating database queries to test error handling or edge cases.
- Replacing third-party libraries (e.g.,
axios,stripe) during unit test runs.
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:
- You want to test that a method on an object is triggered, but the execution of that method is harmless and deterministic.
- You want to temporarily track a method during a specific test block
and then restore the original behavior using
mockRestore().
Use a Mock When:
- The dependency involves I/O operations, network traffic, or state mutations outside the current scope.
- The real implementation is slow or non-deterministic (e.g., timers, random number generators).
- 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
- Clean Up After Tests: Always clear or restore your
spies and mocks in test lifecycle hooks (e.g.,
afterEach) to prevent cross-test contamination. Methods likejest.clearAllMocks()andjest.restoreAllMocks()ensure each test runs in isolation. - Do Not Over-Mock: Mocking internal helper functions of the system under test can make tests brittle. Focus your mocks on external boundaries and side effects instead.
- Verify Specificity: Assert not only that a function was called, but also verify the exact arguments passed to prevent false-positive test results.