DEV Community

GeekyAnts Inc for GeekyAnts

Posted on • Originally published at geekyants.com

How to Mock in Integration Tests: Tools and Implementation

This article was originally published on the GeekyAnts Blog. Author: Nilesh Kumar, Software Engineer II at GeekyAnts.


Having established when and why to mock in integration tests, it's time to explore the practical implementation of mocking strategies. This comprehensive guide covers the essential tools, techniques, and step-by-step processes for implementing effective mocks in your integration tests. We'll dive deep into popular mocking libraries, provide detailed examples, and explore advanced techniques for maintaining reliable and accurate mocks.


Essential Mocking Tools and Libraries

The JavaScript ecosystem offers several powerful tools for implementing mocks in integration tests. Each tool serves different purposes and excels in specific scenarios. Understanding their strengths and use cases will help you choose the right tool for your specific testing needs.


Nock: HTTP Request Mocking

Nock is the most popular and powerful HTTP request mocking library for Node.js. It allows you to intercept and mock HTTP requests at the network level, making it ideal for testing applications that interact with external APIs.

Key Features:

  • Intercepts HTTP requests at the network level
  • Supports complex request matching patterns
  • Provides detailed request/response validation
  • Offers recording and playback capabilities
  • Integrates seamlessly with all testing frameworks
  • Supports both REST and GraphQL APIs

When to Use Nock:

  • Testing interactions with external REST APIs
  • Mocking third-party services like payment gateways
  • Testing error handling for HTTP failures
  • Validating request formats and headers
  • Creating deterministic responses for external services

Basic Nock Usage:

const nock = require('nock');

// Mock a GET request
nock('https://api.example.com')
  .get('/users/1')
  .reply(200, {
    id: 1,
    name: 'John Doe',
    email: 'john@example.com'
  });

// Mock a POST request with request body matching
nock('https://api.example.com')
  .post('/users', { name: 'Jane Doe', email: 'jane@example.com' })
  .reply(201, { id: 2, name: 'Jane Doe' });

// Mock with headers
nock('https://api.example.com')
  .get('/protected-resource')
  .matchHeader('Authorization', 'Bearer my-token')
  .reply(200, { data: 'secret' });

// Mock an error response
nock('https://api.example.com')
  .get('/flaky-endpoint')
  .replyWithError('Connection refused');

// Clean up after tests
afterEach(() => {
  nock.cleanAll();
});
Enter fullscreen mode Exit fullscreen mode

Sinon: Comprehensive Function Mocking

Sinon is a versatile library that provides spies, stubs, and mocks for JavaScript functions and objects. It's particularly useful for mocking internal dependencies and complex object interactions.

Key Features:

  • Function spies for monitoring calls
  • Stubs for replacing function behavior
  • Mocks for complex object interactions
  • Fake timers for time-based testing
  • Extensive assertion capabilities
  • Works with any testing framework

When to Use Sinon:

  • Mocking internal service dependencies
  • Testing time-based functionality
  • Spying on function calls and arguments
  • Stubbing complex object methods
  • Testing callback and promise behavior

Basic Sinon Usage:

const sinon = require('sinon');

describe('UserService', () => {
  let sandbox;

  beforeEach(() => {
    sandbox = sinon.createSandbox();
  });

  afterEach(() => {
    sandbox.restore();
  });

  it('should call the email service on registration', async () => {
    // Create a spy
    const emailSpy = sandbox.spy(emailService, 'sendWelcomeEmail');

    await userService.register({ name: 'Alice', email: 'alice@example.com' });

    sinon.assert.calledOnce(emailSpy);
    sinon.assert.calledWith(emailSpy, 'alice@example.com');
  });

  it('should handle email service failure gracefully', async () => {
    // Stub to simulate failure
    sandbox.stub(emailService, 'sendWelcomeEmail').rejects(new Error('SMTP error'));

    const result = await userService.register({ name: 'Bob', email: 'bob@example.com' });

    expect(result.status).toBe('registered_without_email');
  });

  it('should expire tokens after timeout', async () => {
    const clock = sandbox.useFakeTimers();

    const token = await authService.createToken('user-123');
    clock.tick(3600 * 1000); // Advance 1 hour

    const isValid = await authService.validateToken(token);
    expect(isValid).toBe(false);

    clock.restore();
  });
});
Enter fullscreen mode Exit fullscreen mode

Jest Built-in Mocking

Jest provides powerful built-in mocking capabilities that integrate seamlessly with the testing framework. While not as specialized as Nock or Sinon, Jest mocking is convenient for simple scenarios and module-level mocking.

Key Features:

  • Module mocking with automatic mock generation
  • Function mocking with call tracking
  • Timer mocking for time-based tests
  • Snapshot testing for mocked responses
  • Mock clearing and restoration utilities

When to Use Jest Mocking:

  • Simple function mocking scenarios
  • Module-level mocking
  • Quick prototyping of mocks
  • Integration with Jest snapshot testing

Basic Jest Mocking:

// Auto-mock an entire module
jest.mock('./emailService');

// Manual mock with implementation
jest.mock('./paymentGateway', () => ({
  charge: jest.fn().mockResolvedValue({ success: true, transactionId: 'txn_123' }),
  refund: jest.fn().mockResolvedValue({ success: true }),
}));

describe('OrderService', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should process payment on order placement', async () => {
    const { charge } = require('./paymentGateway');

    await orderService.placeOrder({ userId: 1, items: [...], total: 99.99 });

    expect(charge).toHaveBeenCalledWith({
      amount: 99.99,
      currency: 'USD',
      userId: 1,
    });
  });

  it('should handle payment failure', async () => {
    const { charge } = require('./paymentGateway');
    charge.mockRejectedValueOnce(new Error('Card declined'));

    await expect(
      orderService.placeOrder({ userId: 1, items: [...], total: 99.99 })
    ).rejects.toThrow('Payment failed');
  });
});
Enter fullscreen mode Exit fullscreen mode

Step-by-Step Implementation Guide

Let's walk through a comprehensive example implementing integration tests with strategic mocking for a user notification system.

Application Architecture

Our example notification system includes:

  • NotificationService — orchestrates sending notifications
  • EmailProvider — external SMTP/email API
  • SMSProvider — external SMS gateway
  • UserRepository — database layer for user data
  • NotificationLog — records sent notifications

Step 1: Test Environment Setup

// test/setup.js
const nock = require('nock');
const { MongoMemoryServer } = require('mongodb-memory-server');

let mongoServer;

beforeAll(async () => {
  // Spin up in-memory MongoDB
  mongoServer = await MongoMemoryServer.create();
  process.env.MONGO_URI = mongoServer.getUri();

  // Disable real HTTP calls during tests
  nock.disableNetConnect();
  nock.enableNetConnect('127.0.0.1'); // allow localhost
});

afterAll(async () => {
  await mongoServer.stop();
  nock.enableNetConnect();
});

afterEach(() => {
  nock.cleanAll();
});
Enter fullscreen mode Exit fullscreen mode

Step 2: Basic Integration Test with Mocking

// test/notification.integration.test.js
const request = require('supertest');
const nock = require('nock');
const app = require('../src/app');
const { seedUser } = require('./helpers/seed');

describe('POST /notifications/send', () => {
  let user;

  beforeEach(async () => {
    user = await seedUser({ email: 'user@example.com', phone: '+1234567890' });
  });

  it('should send email and SMS notifications successfully', async () => {
    // Mock email provider
    nock('https://api.sendgrid.com')
      .post('/v3/mail/send')
      .reply(202, { message: 'Accepted' });

    // Mock SMS provider
    nock('https://api.twilio.com')
      .post(`/2010-04-01/Accounts/${process.env.TWILIO_SID}/Messages.json`)
      .reply(201, { sid: 'SM123', status: 'queued' });

    const response = await request(app)
      .post('/notifications/send')
      .send({ userId: user.id, message: 'Your order has shipped!' })
      .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`);

    expect(response.status).toBe(200);
    expect(response.body).toMatchObject({
      email: { status: 'sent' },
      sms: { status: 'queued' },
    });
  });

  it('should still send SMS if email provider fails', async () => {
    nock('https://api.sendgrid.com')
      .post('/v3/mail/send')
      .reply(500, { error: 'Internal Server Error' });

    nock('https://api.twilio.com')
      .post(`/2010-04-01/Accounts/${process.env.TWILIO_SID}/Messages.json`)
      .reply(201, { sid: 'SM456', status: 'queued' });

    const response = await request(app)
      .post('/notifications/send')
      .send({ userId: user.id, message: 'Your order has shipped!' })
      .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`);

    expect(response.status).toBe(207); // Partial success
    expect(response.body.email.status).toBe('failed');
    expect(response.body.sms.status).toBe('queued');
  });
});
Enter fullscreen mode Exit fullscreen mode

Step 3: Advanced Mocking Scenarios

Dynamic Response Generation:

// Respond differently based on request body
nock('https://api.sendgrid.com')
  .post('/v3/mail/send', (body) => body.to === 'vip@example.com')
  .reply(202, { priority: 'high' });

nock('https://api.sendgrid.com')
  .post('/v3/mail/send')
  .reply(202, { priority: 'normal' });
Enter fullscreen mode Exit fullscreen mode

Simulating Rate Limits:

// First 3 calls succeed, then rate limit kicks in
nock('https://api.sendgrid.com')
  .post('/v3/mail/send')
  .times(3)
  .reply(202)
  .post('/v3/mail/send')
  .reply(429, { error: 'Too Many Requests' });
Enter fullscreen mode Exit fullscreen mode

Conditional Mocking Based on Environment:

const mockExternalServices = () => {
  if (process.env.NODE_ENV === 'test') {
    nock('https://api.sendgrid.com')
      .persist()
      .post('/v3/mail/send')
      .reply(202);

    nock('https://api.twilio.com')
      .persist()
      .post(/Messages\.json$/)
      .reply(201, { status: 'queued' });
  }
};
Enter fullscreen mode Exit fullscreen mode

Step 4: Performance Testing with Mocks

it('should handle 100 concurrent notification requests', async () => {
  nock('https://api.sendgrid.com')
    .post('/v3/mail/send')
    .times(100)
    .reply(202);

  nock('https://api.twilio.com')
    .post(/Messages\.json$/)
    .times(100)
    .reply(201, { status: 'queued' });

  const requests = Array.from({ length: 100 }, (_, i) =>
    request(app)
      .post('/notifications/send')
      .send({ userId: `user-${i}`, message: 'Test notification' })
      .set('Authorization', `Bearer ${process.env.TEST_API_KEY}`)
  );

  const start = Date.now();
  const responses = await Promise.all(requests);
  const duration = Date.now() - start;

  expect(responses.every((r) => r.status === 200)).toBe(true);
  expect(duration).toBeLessThan(5000); // Should complete in under 5 seconds
});
Enter fullscreen mode Exit fullscreen mode

Advanced Mocking Techniques

Request Recording and Playback

Use Nock's record mode to capture real API responses and replay them in tests — great for staying in sync with real API behaviour without hitting production systems on every run.

// Record mode (run once against real APIs)
if (process.env.RECORD_MOCKS === 'true') {
  nock.recorder.rec({ output_objects: true });
}

// Playback mode (default)
const recordings = require('./fixtures/api-recordings.json');
nock.define(recordings);
Enter fullscreen mode Exit fullscreen mode

Mock Validation and Maintenance

Always assert that all registered mocks were actually called — this catches dead code and stale mocks early:

afterEach(() => {
  // Verify all registered nock interceptors were used
  expect(nock.pendingMocks()).toHaveLength(0);
  nock.cleanAll();
});
Enter fullscreen mode Exit fullscreen mode

Best Practices for Mock Implementation

1. Keep Mocks Simple and Focused
Each mock should represent exactly one scenario. Avoid overloading a single mock with conditional logic — create separate test cases instead.

2. Use Realistic Data and Delays
Make your mocks representative of real system behaviour. Add response delays where appropriate and use production-like payloads:

nock('https://api.sendgrid.com')
  .post('/v3/mail/send')
  .delay(120) // Simulate realistic network latency
  .reply(202, { message: 'Accepted' });
Enter fullscreen mode Exit fullscreen mode

3. Mock at the Appropriate Level

  • Use Nock for external HTTP services
  • Use Sinon stubs for internal module dependencies
  • Use in-memory databases rather than mocking the DB layer

4. Document Mock Decisions
Leave comments explaining why a mock was introduced, what real behaviour it replaces, and when it should be revisited:

// Mocking SendGrid because the free tier has strict rate limits
// in CI. Review if we upgrade to a paid plan — real integration
// tests would be preferable.
nock('https://api.sendgrid.com')
  .post('/v3/mail/send')
  .reply(202);
Enter fullscreen mode Exit fullscreen mode

Conclusion

Implementing effective mocks in integration tests requires careful consideration of tools, techniques, and maintenance strategies. The key is to use mocks strategically to eliminate problematic external dependencies while preserving the authentic integrations that provide the most value.

  • Nock excels at HTTP request mocking and is essential for testing external API integrations.
  • Sinon provides comprehensive function and object mocking capabilities for internal dependencies.
  • Jest's built-in mocking works well for simple scenarios and integrates seamlessly with the testing framework.

By following the patterns and best practices in this guide, you can create integration tests that give you genuine confidence in your system's behaviour while remaining practical to execute and maintain.

Remember: mocks are tools to enable effective testing, not goals in themselves. Always evaluate whether your mocking strategy is serving your testing objectives — and adjust as your application evolves.


Originally published on GeekyAnts Blog by Nilesh Kumar.

Top comments (0)