NeuroAgent

Mock vs Stub: Key Differences in Unit Testing

Discover the fundamental differences between mocks and stubs in unit testing. Learn when to use each test double, their characteristics, and practical examples for effective test isolation.

Question

What’s the difference between a mock and stub in unit testing?

I’ve read various articles about mocking vs stubbing in testing, including Martin Fowler’s ‘Mocks Aren’t Stubs’, but still don’t understand the difference. Can someone explain the key distinctions between mocks and stubs, their use cases, and when to use each in test scenarios?

NeuroAgent

Mocks and stubs are both test doubles used in unit testing to isolate the code under test, but they serve different purposes: mocks verify interactions and behavior, while stubs provide predefined responses to method calls. The key distinction lies in their testing approach - mocks focus on verifying “how” the code interacts with dependencies, while stubs focus on providing “what” dependencies return. Understanding this difference helps developers choose the right test double for specific scenarios.

Contents

Understanding Test Doubles

Test doubles are objects that stand in for real dependencies during testing, allowing developers to isolate the code under test from external systems. The term “test double” was coined by Gerard Meszaros in his book xUnit Test Patterns and is an analogy from the film industry where stunt doubles stand in for actors.

There are several types of test doubles, including:

  • Stubs: Provide canned responses to method calls
  • Mocks: Verify interactions and behavior
  • Fakes: Simplified working implementations
  • Dummies: Objects passed but never used
  • Spies: Track method calls without affecting behavior

Each type serves a specific purpose in testing, with stubs and mocks being the most commonly used in modern testing practices.

What is a Stub?

A stub is a simplified implementation of a dependency that returns predefined responses to method calls. According to PFLB, stubs are “used in mock testing to simulate the behavior of real objects and ensure that specific paths of code are executed during testing.”

Characteristics of stubs:

  • Return predefined values for specific method calls
  • Don’t verify how methods are called
  • Focus on state-based testing
  • Are typically passive objects
  • Provide predictable, consistent responses

For example, if you’re testing a user service that depends on a database, you might create a stub database connection that returns specific user data when queried, without actually connecting to a real database.

javascript
// Stub example
const databaseStub = {
  findUser: (id) => {
    if (id === '123') return { id: '123', name: 'John Doe' };
    if (id === '456') return { id: '456', name: 'Jane Smith' };
    return null;
  }
};

Stubs are particularly useful when you need to test different scenarios by varying the responses from dependencies, such as testing how your code handles missing data, error conditions, or specific data states.

What is a Mock?

A mock object is more sophisticated than a stub - it not only provides responses but also verifies that the code under test interacts with it correctly. As Harness explains, mocks isolate “the system under test from unreliable or unfinished dependencies” and can verify expected interactions.

Characteristics of mocks:

  • Verify that methods are called with specific parameters
  • Track the number and order of method calls
  • Can throw exceptions when unexpected interactions occur
  • Focus on behavior-based testing
  • Are typically active objects that assert their expectations

For example, using a mock to verify that an email service is called exactly once with the correct parameters when a user registers:

javascript
// Mock example
const emailServiceMock = {
  sendWelcomeEmail: jest.fn(),
  verify: (expectedCalls) => {
    expect(emailServiceMock.sendWelcomeEmail).toHaveBeenCalledTimes(expectedCalls.count);
    expect(emailServiceMock.sendWelcomeEmail).toHaveBeenCalledWith(...expectedCalls.args);
  }
};

Mocks become powerful when you need to verify that your code follows specific interaction patterns, such as ensuring that cleanup operations are performed, or that certain methods are called in the correct order.

Key Differences Between Mocks and Stubs

The fundamental difference between mocks and stubs lies in their testing philosophy and what they verify:

Aspect Stubs Mocks
Purpose Provide predefined responses Verify interactions and behavior
Testing Approach State-based Behavior-based
Verification Check return values and state Check method calls and order
Complexity Simple, passive objects Complex, active objects
Focus “What” the dependency returns “How” the code interacts with dependency
Setup Configure return values Configure expectations and verifications

As noted in MoldStud’s testing guide, “If you want to verify interaction without executing actual code, create stubs that return predefined values. On the other hand, spies enable you to track if a method was called, how many times, and with what arguments.”

This distinction is crucial: stubs help you test your code’s logic by controlling what dependencies return, while mocks help you test your code’s behavior by verifying how it uses those dependencies.


The Fowler Perspective

While Martin Fowler’s original “Mocks Aren’t Stubs” article isn’t directly available in our search results, the distinction he popularized is fundamental to modern testing practices. Fowler argued that:

  1. Stubs are about providing test data and controlling what dependencies return
  2. Mocks are about verifying interactions and ensuring the code under test uses dependencies correctly

This distinction helps prevent what Fowler called “mockist” testing (over-reliance on mocks for everything) versus “classicist” testing (more focused on state verification).

When to Use Stubs

Stubs are the right choice in these scenarios:

Testing Different Scenarios

When you need to test how your code handles various responses from dependencies:

  • Missing data
  • Error conditions
  • Different data states
  • Edge cases

For example, testing a user registration service with a stub that returns different user existence results:

javascript
// Stub for testing different scenarios
const userExistsStub = {
  checkUserExists: (email) => {
    // Simulate different scenarios
    if (email === 'existing@example.com') return true;
    if (email === 'error@example.com') throw new Error('Database error');
    return false; // New user
  }
};

Testing State-Based Logic

When your code’s correctness depends on the state it achieves rather than how it achieves it:

  • Calculating totals from multiple data sources
  • Processing and transforming data
  • Generating reports or outputs

External Dependencies

When dealing with:

  • Databases and data stores
  • Web services and APIs
  • File systems
  • Network resources

Stubs allow you to test these scenarios without actually connecting to external systems, making tests faster and more reliable.

When to Use Mocks

Mocks are particularly valuable in these situations:

Verifying Interaction Patterns

When you need to ensure your code uses dependencies correctly:

  • Method is called with correct parameters
  • Method is called expected number of times
  • Methods are called in correct order
  • Cleanup operations are performed

For example, testing that a payment service properly calls the authorization service before processing:

javascript
// Mock for verifying interaction patterns
const authServiceMock = {
  authorize: jest.fn().mockReturnValue(true),
  verify: () => {
    expect(authServiceMock.authorize).toHaveBeenCalledBefore(paymentServiceMock.processPayment);
  }
};

Testing Complex Workflows

For multi-step processes where interaction order matters:

  • User registration flows
  • Payment processing pipelines
  • Data transformation workflows

Behavioral Contracts

When you want to enforce behavioral contracts between components:

  • API contracts between services
  • Integration points between modules
  • Interface implementations

Mocks help ensure that these contracts are maintained, preventing regressions when code changes.

Practical Examples

Let’s explore some practical examples to illustrate when to use mocks versus stubs.

Example 1: User Registration Service

Scenario: A user registration service that needs to:

  1. Check if user already exists
  2. Hash the password
  3. Save user to database
  4. Send welcome email

Using Stubs:

javascript
describe('User Registration Service with Stubs', () => {
  it('should register new user', () => {
    // Stub dependencies
    const userRepoStub = {
      findByEmail: () => null, // User doesn't exist
      save: (user) => user // Successfully saves
    };
    
    const passwordHasherStub = {
      hash: (password) => 'hashed_password'
    };
    
    const emailServiceStub = {
      sendWelcomeEmail: () => {} // Does nothing
    };
    
    // Test the service logic
    const service = new RegistrationService(
      userRepoStub, passwordHasherStub, emailServiceStub
    );
    
    const result = service.register('test@example.com', 'password123');
    
    // Verify the result state
    expect(result.success).toBe(true);
    expect(result.user.email).toBe('test@example.com');
  });
});

Using Mocks:

javascript
describe('User Registration Service with Mocks', () => {
  it('should call dependencies in correct order', () => {
    // Mock dependencies with expectations
    const userRepoMock = {
      findByEmail: jest.fn(),
      save: jest.fn()
    };
    
    const passwordHasherMock = {
      hash: jest.fn().mockReturnValue('hashed_password')
    };
    
    const emailServiceMock = {
      sendWelcomeEmail: jest.fn()
    };
    
    const service = new RegistrationService(
      userRepoMock, passwordHasherMock, emailServiceMock
    );
    
    service.register('test@example.com', 'password123');
    
    // Verify interactions
    expect(userRepoMock.findByEmail).toHaveBeenCalledWith('test@example.com');
    expect(passwordHasherMock.hash).toHaveBeenCalledWith('password123');
    expect(userRepoMock.save).toHaveBeenCalledWith(
      expect.objectContaining({ email: 'test@example.com' })
    );
    expect(emailServiceMock.sendWelcomeEmail).toHaveBeenCalled();
    
    // Verify order of calls
    expect(userRepoMock.findByEmail).toHaveBeenCalledBefore(passwordHasherMock.hash);
    expect(passwordHasherMock.hash).toHaveBeenCalledBefore(userRepoMock.save);
  });
});

Example 2: Payment Processing Service

Scenario: A payment service that processes orders through multiple steps.

Stub Approach (focusing on state):

javascript
describe('Payment Processing with Stubs', () => {
  it('should process payment successfully', () => {
    // Stub payment gateway to return success
    const paymentGatewayStub = {
      process: () => ({ success: true, transactionId: 'txn_123' })
    };
    
    const orderRepositoryStub = {
      updateStatus: () => {} // Updates order status
    };
    
    const service = new PaymentService(
      paymentGatewayStub, orderRepositoryStub
    );
    
    const order = { id: 'ord_456', amount: 100 };
    const result = service.processPayment(order);
    
    // Verify final state
    expect(result.success).toBe(true);
    expect(result.transactionId).toBeDefined();
  });
});

Mock Approach (focusing on behavior):

javascript
describe('Payment Processing with Mocks', () => {
  it('should handle payment failures correctly', () => {
    // Mock payment gateway to throw exception
    const paymentGatewayMock = {
      process: jest.fn().mockImplementation(() => {
        throw new Error('Insufficient funds');
      })
    };
    
    const orderRepositoryMock = {
      updateStatus: jest.fn()
    };
    
    const service = new PaymentService(
      paymentGatewayMock, orderRepositoryMock
    );
    
    const order = { id: 'ord_456', amount: 100 };
    
    expect(() => service.processPayment(order)).toThrow('Insufficient funds');
    
    // Verify that order status was updated to failed
    expect(orderRepositoryMock.updateStatus).toHaveBeenCalledWith(
      order.id, 'payment_failed'
    );
  });
});

Best Practices

Choosing Between Mocks and Stubs

  1. Start with Stubs: Begin with stubs for most scenarios, as they’re simpler and more maintainable
  2. Use Mocks Sparingly: Only use mocks when you specifically need to verify interactions
  3. Consider the Testing Pyramid: Use more unit tests with stubs, fewer integration tests with mocks
  4. Avoid Over-mocking: Don’t mock everything - focus on the most critical interactions

Implementation Tips

  1. Clear Naming: Use descriptive names for test doubles (e.g., userRepositoryStub, paymentServiceMock)
  2. Setup and Teardown: Properly initialize and clean up test doubles
  3. Documentation: Comment why you’re using a specific type of test double
  4. Consistency: Be consistent within your test suite

Common Pitfalls to Avoid

  1. Mocking Too Much: This can lead to brittle tests that break with implementation changes
  2. State vs Behavior Confusion: Don’t use mocks when you only need to test state
  3. Over-specification: Avoid testing implementation details that might change
  4. Ignoring Test Speed: Mocks can sometimes make tests slower due to verification overhead

By understanding the fundamental differences between mocks and stubs, you can write more effective tests that focus on the right aspects of your code. Stubs help you control what dependencies return, while mocks help you verify how your code uses those dependencies. The key is to choose the right tool for the specific testing scenario.

Sources

  1. PFLB - What is Mock Testing?: Benefits & How Does It Work
  2. Harness - What is Mock Testing?
  3. GFU Wiki - Was ist Mocking?
  4. MoldStud - Mocking and Stubbing Techniques in Node.js Testing with Mocha and Chai
  5. dblp - Martin Fowler
  6. Lotus QA - Unit Testing vs Functional Testing: Comprehensive Comparison

Conclusion

Understanding the difference between mocks and stubs is crucial for writing effective unit tests. Here are the key takeaways:

  1. Stubs provide predefined responses and focus on state-based testing, making them ideal for testing how your code handles different scenarios and data states.

  2. Mocks verify interactions and focus on behavior-based testing, making them perfect for ensuring your code uses dependencies correctly in terms of method calls, parameters, and order.

  3. Choose based on what you’re testing: Use stubs when you care about “what” happens to the state, and mocks when you care about “how” interactions occur.

  4. Balance your approach: Don’t overuse mocks - they can make tests brittle. Start with stubs and only introduce mocks when you specifically need to verify interactions.

  5. Consider the testing context: For simple scenarios, stubs often suffice. For complex workflows or behavioral contracts, mocks become more valuable.

By mastering both techniques and knowing when to apply each, you’ll create tests that are more maintainable, focused, and effective at catching the right kinds of bugs in your code.