Skip to main content

Express Mocking

Introduction

When testing Express.js applications, you often need to isolate the code you're testing from external dependencies like databases, third-party APIs, or even other parts of your application. This is where mocking comes in. Mocking allows you to replace real dependencies with fake versions that you control, making your tests more reliable, faster, and focused on the specific functionality you want to test.

In this guide, we'll explore how to use mocking effectively in Express applications, examining various techniques and tools to simulate dependencies and make your tests more maintainable and efficient.

What is Mocking?

Mocking is the practice of creating substitute objects (mocks) that simulate the behavior of real objects in a controlled way. In testing, mocks allow you:

  1. Isolate the code under test - By replacing external dependencies with mocks, you can test your code in isolation.
  2. Control test conditions - Mocks let you easily simulate success scenarios, error conditions, network failures, etc.
  3. Speed up tests - Tests run faster when they don't need to interact with real databases or APIs.
  4. Test difficult scenarios - You can easily test error handling by making your mocks throw errors on demand.

Common Libraries for Mocking in Express

Before diving into examples, let's familiarize ourselves with popular mocking libraries for JavaScript and Express:

  1. Jest - Full-featured testing framework with built-in mocking capabilities
  2. Sinon.js - Standalone library for spies, stubs, and mocks
  3. Nock - Library for mocking HTTP requests
  4. Supertest - For testing HTTP endpoints
  5. Proxyquire - For replacing dependencies during testing

Basic Mocking Techniques

1. Mocking Express Request and Response

When unit testing Express route handlers, you'll often want to mock the request and response objects:

javascript
const httpMocks = require('node-mocks-http');

// Function to test
const userController = require('../controllers/userController');

describe('User Controller', () => {
test('getUser returns user data', () => {
// Create mock request
const req = httpMocks.createRequest({
method: 'GET',
url: '/api/users/123',
params: {
id: '123'
}
});

// Create mock response
const res = httpMocks.createResponse();

// Call the function with mocked req, res
userController.getUser(req, res);

// Get data sent to response
const data = res._getJSONData();

// Assertions
expect(res.statusCode).toBe(200);
expect(data.id).toBe('123');
});
});

2. Mocking Database Operations

Database operations are common candidates for mocking. Let's see how to mock MongoDB operations with Jest:

javascript
// User model that uses mongoose
const User = require('../models/user');

// Controller that uses the model
const userController = require('../controllers/userController');

// Mock the User model
jest.mock('../models/user');

describe('User Controller with mocked database', () => {
test('getAllUsers returns all users', async () => {
// Setup mock implementation
const mockUsers = [{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }];
User.find.mockResolvedValue(mockUsers);

const req = {};
const res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};

// Call the controller function
await userController.getAllUsers(req, res);

// Assertions
expect(User.find).toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(mockUsers);
});
});

3. Mocking External API Calls

When your Express app makes HTTP requests to external services, you can mock these using nock:

javascript
const nock = require('nock');
const weatherController = require('../controllers/weatherController');

describe('Weather Controller', () => {
test('getWeather fetches from weather API and returns data', async () => {
// Mock the external API
const mockWeatherData = { temperature: 72, condition: 'Sunny' };

// Intercept HTTP GET requests to the weather API
nock('https://api.weatherservice.com')
.get('/data')
.query({ city: 'London' })
.reply(200, mockWeatherData);

const req = { query: { city: 'London' } };
const res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};

// Call the controller method
await weatherController.getWeather(req, res);

// Assertions
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(mockWeatherData);
});
});

Advanced Mocking Techniques

Mocking Middleware

Express applications often use middleware. Here's how to mock middleware for testing:

javascript
const authMiddleware = require('../middleware/auth');
const httpMocks = require('node-mocks-http');

jest.mock('../middleware/auth');

describe('Protected Route', () => {
test('should pass with authenticated user', () => {
// Mock implementation of auth middleware
authMiddleware.mockImplementation((req, res, next) => {
req.user = { id: '123', role: 'admin' };
next();
});

const req = httpMocks.createRequest();
const res = httpMocks.createResponse();
const next = jest.fn();

// Call the middleware
authMiddleware(req, res, next);

// Assert middleware effects
expect(req.user).toBeDefined();
expect(req.user.id).toBe('123');
expect(next).toHaveBeenCalled();
});
});

Mocking Express Router

You can also mock the Express router to test route registration:

javascript
const express = require('express');
const userRoutes = require('../routes/userRoutes');

// Mock express router
jest.mock('express', () => {
const mockRouter = {
get: jest.fn(),
post: jest.fn(),
put: jest.fn(),
delete: jest.fn()
};

return {
Router: jest.fn(() => mockRouter)
};
});

describe('User Routes', () => {
test('should define correct routes', () => {
// Load routes (which will use our mocked Router)
require('../routes/userRoutes');

// Get the mock router instance
const router = express.Router();

// Assertions to verify routes are registered
expect(router.get).toHaveBeenCalledWith('/users', expect.any(Function));
expect(router.post).toHaveBeenCalledWith('/users', expect.any(Function));
expect(router.put).toHaveBeenCalledWith('/users/:id', expect.any(Function));
});
});

Real-World Example: Testing an Authentication System

Let's put everything together with a more comprehensive example of testing an authentication system:

javascript
// auth.test.js
const httpMocks = require('node-mocks-http');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const User = require('../models/user');
const authController = require('../controllers/authController');

// Mock dependencies
jest.mock('jsonwebtoken');
jest.mock('bcrypt');
jest.mock('../models/user');

describe('Authentication Controller', () => {
beforeEach(() => {
// Clear all mocks before each test
jest.clearAllMocks();
});

describe('login', () => {
test('should login successfully with correct credentials', async () => {
// Mock request with login data
const req = httpMocks.createRequest({
method: 'POST',
body: {
email: '[email protected]',
password: 'password123'
}
});

const res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};

// Mock user retrieval
const mockUser = {
_id: '123',
email: '[email protected]',
password: 'hashedPassword'
};

User.findOne.mockResolvedValue(mockUser);

// Mock password comparison
bcrypt.compare.mockResolvedValue(true);

// Mock token generation
const mockToken = 'fake-jwt-token';
jwt.sign.mockReturnValue(mockToken);

// Call the login function
await authController.login(req, res);

// Assertions
expect(User.findOne).toHaveBeenCalledWith({ email: '[email protected]' });
expect(bcrypt.compare).toHaveBeenCalledWith('password123', 'hashedPassword');
expect(jwt.sign).toHaveBeenCalledWith(
{ userId: '123' },
expect.any(String),
{ expiresIn: expect.any(String) }
);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({
token: mockToken,
userId: '123'
});
});

test('should return 401 with incorrect password', async () => {
// Setup request
const req = httpMocks.createRequest({
method: 'POST',
body: {
email: '[email protected]',
password: 'wrongpassword'
}
});

const res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};

// Mock user retrieval
User.findOne.mockResolvedValue({
_id: '123',
email: '[email protected]',
password: 'hashedPassword'
});

// Mock password comparison (fails)
bcrypt.compare.mockResolvedValue(false);

// Call the login function
await authController.login(req, res);

// Assertions
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('Invalid credentials')
})
);
});
});

describe('register', () => {
test('should register a new user successfully', async () => {
// Mock request
const req = httpMocks.createRequest({
method: 'POST',
body: {
email: '[email protected]',
password: 'newpassword',
name: 'New User'
}
});

const res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};

// Mock user existence check
User.findOne.mockResolvedValue(null);

// Mock password hashing
bcrypt.hash.mockResolvedValue('hashedNewPassword');

// Mock user creation
const mockCreatedUser = {
_id: '456',
email: '[email protected]',
name: 'New User'
};

User.create.mockResolvedValue(mockCreatedUser);

// Call the register function
await authController.register(req, res);

// Assertions
expect(User.findOne).toHaveBeenCalledWith({ email: '[email protected]' });
expect(bcrypt.hash).toHaveBeenCalledWith('newpassword', expect.any(Number));
expect(User.create).toHaveBeenCalledWith({
email: '[email protected]',
password: 'hashedNewPassword',
name: 'New User'
});
expect(res.status).toHaveBeenCalledWith(201);
expect(res.json).toHaveBeenCalledWith(expect.objectContaining({
message: expect.any(String),
userId: '456'
}));
});
});
});

Best Practices for Mocking

  1. Mock at the right level - Mock at module boundaries rather than internal functions when possible.

  2. Avoid excessive mocking - Excessive mocking can lead to tests that don't effectively test integration between components.

  3. Verify mock calls - Always verify that your mocks were called with the expected arguments.

  4. Clean up after tests - Reset or restore mocks between tests to avoid tests affecting each other.

  5. Be consistent with real behavior - Make your mocks behave similar to real implementations to catch integration issues early.

  6. Don't mock the system under test - Only mock dependencies, not the code you're trying to test.

  7. Use spies to verify interactions - When you want to verify function calls without changing behavior, use spies.

Common Mocking Mistakes to Avoid

  1. Over-mocking - Mocking too many parts of your system can lead to tests that pass but don't reflect real-world behavior.

  2. Mocking without verification - Always verify that your mocks were called as expected.

  3. Complex mock implementations - Keep your mock implementations simple and focused on the test needs.

  4. Forgetting to reset mocks - Failing to reset mocks between tests can cause unexpected behaviors.

  5. Not handling asynchronous mocks properly - Ensure you're properly resolving or rejecting promises in your mocks.

Summary

Mocking is an essential technique in testing Express applications that helps you create reliable, fast, and focused tests. By replacing real dependencies with controlled mocks, you can isolate components, test edge cases, and ensure your application behaves as expected in various scenarios.

We've covered:

  • The fundamentals of mocking in Express applications
  • How to mock Express request and response objects
  • Techniques for mocking databases and external API calls
  • Advanced mocking of middleware and routers
  • A real-world example of testing an authentication system

With these tools and techniques, you can create comprehensive test suites that give you confidence in your Express applications.

Additional Resources

  1. Jest Documentation
  2. Sinon.js Documentation
  3. Node Mocks HTTP
  4. Nock Documentation
  5. SuperTest Documentation

Exercises

  1. Create a mock for a user service that simulates fetching, creating, and updating user data.

  2. Write tests for an Express route that handles file uploads by mocking the file storage service.

  3. Test an error handling middleware by mocking errors in various request handlers.

  4. Create a test suite for a payment processing API where you mock the external payment gateway.

  5. Implement tests for a caching middleware by mocking the cache storage mechanism.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)