Mocking and Stubbing: Testing with Dependencies

Published on December 15, 2025 | M.E.A.N Stack Development
WhatsApp Us

Mocking and Stubbing: A Beginner's Guide to Testing with Dependencies

You've mastered writing simple unit tests. Your functions that add numbers or format strings pass with flying colors. But then you try to test a function that sends an email, fetches data from a database, or calls a third-party API. Suddenly, your test fails because the mail server is down, the database isn't set up in your test environment, or you're getting rate-limited by the API. Welcome to the core challenge of modern software testing: dealing with dependencies.

This is where mocking and stubbing become essential skills. They are techniques that allow you to isolate the code you're testing by replacing its dependencies with controlled, fake versions. This guide will demystify these concepts, show you why they're critical for reliable tests, and provide practical examples you can apply immediately. Unlike purely theoretical overviews, we'll focus on the hands-on application you need to build testable, robust software—the kind of practical skill emphasized in practical development courses that prepare you for real-world projects.

Key Takeaway

Mocking and Stubbing are forms of "Test Doubles"—fake objects used in place of real dependencies during testing. They allow you to:

  • Isolate the unit of code you're testing.
  • Create predictable test environments (no more failed tests due to network issues).
  • Verify interactions (e.g., "Was this function called with the right arguments?").
  • Simulate edge cases and errors easily (e.g., "What happens if the API returns a 500 error?").

Why Isolating Dependencies is Non-Negotiable

A true unit test should test one unit of behavior in isolation. If your test fails because an external service changed, your database is slow, or a file is missing, it's not a reliable unit test. It's an integration test (which is also valuable, but serves a different purpose). Unreliable tests lead to "test flakiness," where developers start ignoring test results—defeating the entire purpose of having a test suite.

By using test doubles like mocks and stubs, you transform slow, brittle tests into fast, deterministic ones. Your test suite becomes a safety net you can actually trust.

Understanding the Test Double Family

The term "test double" is the umbrella category for all fake objects used in testing. Think of them as stunt doubles for your software components. Gerard Meszaros, who coined the term, defined several specific types. The two most commonly used—and often confused—are Mocks and Stubs.

Stubs: The Controlled Responders

A stub is a test double that provides pre-programmed answers to calls made during the test. It's used to control the input into the system under test. You don't care if the stub's methods are called; you care that your code behaves correctly given a specific response from the dependency.

Real-World Example: Imagine testing a function `getUserDiscount(userId)` that calls a `UserService.fetchUserTier(userId)` dependency. The real service queries a database. For your test, you can stub `fetchUserTier` to return `"premium"` for a test ID, allowing you to verify that the discount calculation is correct for premium users without ever touching the database.

Mocks: The Behavior Verifiers

A mock is a test double that you use to verify interactions. You set expectations on the mock before the test runs (e.g., "this method must be called once with these exact arguments"). The mock records how it's called, and the test framework asserts those expectations were met.

Real-World Example: Testing a function `notifyUser(userId, message)` that calls an `EmailService.send()` dependency. You don't want to send real emails during testing. Instead, you create a mock of `EmailService` and set an expectation that `send()` is called exactly once with the user's email address and the correct message body. The test verifies the interaction happened correctly.

Stub vs. Mock: A Simple Analogy

Think of ordering a taxi (your code under test) to go to the airport (the desired outcome).

  • Using a Stub: You replace the GPS system (dependency) with one that always says the airport is 20 miles away. You test if the taxi calculates the correct fare and ETA based on that 20-mile input.
  • Using a Mock: You replace the taxi's logbook (dependency). After the ride, you check the logbook to verify it recorded a trip starting at your location and ending at the airport. You're testing the act of recording.
In practice, modern libraries often blur this line, but understanding the intent is crucial for writing clear tests.

Putting It Into Practice: Examples with Jest and Sinon

Let's look at practical code. Jest is a popular JavaScript testing framework with built-in mocking capabilities. Sinon is a standalone library that provides robust test doubles, often used with other frameworks like Mocha.

Stubbing with Jest

Jest provides `jest.fn()` to create mock functions, which can easily be configured as stubs.

// paymentProcessor.js
export function processPayment(orderId, paymentGateway) {
    const success = paymentGateway.charge(orderId);
    if (success) {
        return `Payment for ${orderId} processed.`;
    }
    throw new Error('Payment failed');
}

// paymentProcessor.test.js
import { processPayment } from './paymentProcessor';

test('processPayment returns success message on successful charge', () => {
    // 1. Create a stub for the dependency
    const mockGateway = {
        charge: jest.fn(() => true) // Stub: always returns true
    };

    // 2. Execute function with the stub
    const result = processPayment('order-123', mockGateway);

    // 3. Assert on the result (behavior due to the stub's response)
    expect(result).toBe('Payment for order-123 processed.');
    // Optional: We could also check the stub was called
    expect(mockGateway.charge).toHaveBeenCalledWith('order-123');
});

test('processPayment throws error on failed charge', () => {
    const mockGateway = {
        charge: jest.fn(() => false) // Stub: now returns false to simulate failure
    };

    // Assert that the function throws
    expect(() => processPayment('order-456', mockGateway))
        .toThrow('Payment failed');
});

Mocking and Verifying Interactions with Sinon

Sinon offers more explicit control with `sinon.stub()` and `sinon.mock()`.

// notificationService.js
export class NotificationService {
    constructor(logger) {
        this.logger = logger;
    }
    sendAlert(message) {
        // Complex logic to send alert...
        this.logger.info(`Alert sent: ${message}`);
    }
}

// notificationService.test.js (using Mocha + Sinon)
import sinon from 'sinon';
import { NotificationService } from './notificationService';

describe('NotificationService', () => {
    it('should log an info message when sending an alert', () => {
        // 1. Create a mock logger object
        const mockLogger = {
            info: sinon.stub() // Creates a stub for the 'info' method
        };

        // 2. Create the service with the mock dependency
        const service = new NotificationService(mockLogger);

        // 3. Execute the action
        service.sendAlert('System overload!');

        // 4. Verify the INTERACTION with the mock
        // This is the "mocking" aspect: asserting *how* the dependency was used.
        sinon.assert.calledOnce(mockLogger.info);
        sinon.assert.calledWith(mockLogger.info, 'Alert sent: System overload!');
    });
});

These examples show the transition from theory to executable code—a fundamental principle in effective application-focused training, where concepts are immediately grounded in implementation.

When to Use Mocks vs. Stubs: A Practical Decision Tree

Confused about which to choose? Follow this simple logic:

  1. Are you trying to simulate a specific state or return value from a dependency to make your main code work? → Use a Stub.
    • Example: "Make this database call return an empty array." "Make this API client throw a 404 error."
  2. Are you trying to verify that your code called a dependency in the correct way (with specific arguments, a certain number of times)? → Use a Mock.
    • Example: "Ensure the `save` method was called exactly once." "Verify the email was sent to the correct address."
  3. Do you need to do both? → Most mocking libraries allow you to combine these. In Jest, a `jest.fn()` can be set to return a value (stubbing) and you can later assert on its calls (mocking).

Common Pitfalls and Best Practices

As you start mocking and stubbing, avoid these common mistakes:

  • Over-Mocking: Don't mock every single dependency, especially ones that are simple, stable, and fast. You risk testing your mocks, not your code. Mock external, unstable, or slow services (APIs, databases, file systems, email).
  • Overspecified Tests: Tests that mock internal, private interactions become brittle. If you refactor your code, these tests break even though the overall behavior is correct. Mock at the "seam"—the boundary where your code meets an external system.
  • Ignoring Integration Tests: Mocks and stubs are for unit tests. You still need integration and end-to-end tests to ensure the real dependencies work together correctly.
  • Forgetting to Reset/Restore: In some frameworks, mocks can persist between tests, causing cross-test pollution. Use lifecycle methods like `beforeEach` or `afterEach` to reset your mocks (`jest.clearAllMocks()`, `sinon.restore()`).

Beyond the Basics: Advanced Isolation Techniques

Once you're comfortable with basic mocks and stubs, you can explore more sophisticated patterns:

  • Spies: Wraps a real function to record how it's called without replacing its behavior. Useful for verifying callbacks.
  • Fakes: A lightweight, working implementation of a dependency (e.g., an in-memory database for testing).
  • Dependency Injection (DI): This is the architectural pattern that makes mocking possible. By passing dependencies as parameters (or via a constructor), you enable easy swapping of real implementations for test doubles. Frameworks like Angular have DI built-in, which is why testing Angular services is so straightforward—a topic covered in depth in specialized Angular training programs.

FAQs on Mocking and Stubbing

Beginners often have these questions when starting with test doubles.

"I'm new to testing. Should I learn mocking right away?"
Start with simple unit tests on pure functions (no dependencies). Once you encounter a function that calls an API, reads a file, or interacts with a database, that's your signal to learn mocking and stubbing. It's the natural next step.
"What's the difference between Jest's `jest.fn()`, `jest.spyOn()`, and `jest.mock()`?"
`jest.fn()` creates a brand new mock function from scratch. `jest.spyOn()` attaches a spy to an existing method on an object, allowing you to track its calls and optionally stub its implementation. `jest.mock()` is used for auto-mocking entire modules.
"Isn't mocking just making tests pass without testing real behavior?"
A common concern! Good mocking tests the logic of your code. You provide specific inputs (via stubs) and verify specific outputs or interactions. You still need integration tests for the "real" behavior, but unit tests with mocks ensure each piece works correctly in isolation.
"How do I mock a module that's imported at the top of my file?"
Jest provides module mocking via `jest.mock('./modulePath')`. This is a powerful feature that replaces all exports from that module with automatic mocks before your test code runs. You can then customize those mocks.
"When should I use Sinon over Jest's built-in mocks?"
If your project uses Jest, its built-in mocking is usually sufficient and simpler. Use Sinon if you are using a different test runner (like Mocha or Jasmine) or if you need its more advanced, standalone features.
"Can I mock Date or Math.random() for testing?"
Absolutely! This is a great use case. Jest provides `jest.setSystemTime()` and `jest.spyOn(global.Math, 'random')` to control time and randomness, making tests for time-sensitive or probabilistic logic deterministic.
"My mocked function is being called, but my test says it wasn't. What's wrong?"
This is often a scope/import issue. Ensure you are mocking the exact same module/function that your code under test is importing. Also, check the order of your mock definitions; they often need to be at the top of your test file.
"How do I simulate a promise rejection (like a failed API call) with a stub?"
With Jest, you can use `jest.fn().mockRejectedValue(new Error('API Failed'))`. With Sinon, use `sinon.stub(api, 'fetch').rejects(new Error('API Failed'))`. This allows you to cleanly test your error-handling paths.

Conclusion: Building Confidence Through Isolation

Mastering mocking and stubbing is what separates basic test writers from developers who can build truly resilient, test-driven applications. It allows you to surgically isolate problems, simulate any scenario, and create a test suite that runs in milliseconds, giving you immediate feedback.

Remember, the goal isn't just to make tests pass—it's to design better software. The need to mock dependencies often reveals where your code is too tightly coupled, guiding you towards more modular and maintainable architecture with clear boundaries.

This practical, code-first approach to mastering development concepts—from testing to architecture

Ready to Master Full Stack Development Journey?

Transform your career with our comprehensive full stack development courses. Learn from industry experts with live 1:1 mentorship.