Skip to main content

JavaScript Mocking

Introduction to Mocking in JavaScript

When writing tests for your JavaScript applications, you'll often encounter situations where your code depends on external resources or complex objects that are difficult to control in a test environment. This is where mocking comes in handy.

Mocking is a technique that allows you to replace real implementation of functions, objects, or modules with controlled substitutes. These substitutes, called "mocks," mimic the behavior of the real components but are completely under your control during tests.

In this guide, we'll explore:

  • What mocking is and why it's important in testing
  • Different types of mocks in JavaScript
  • How to implement mocking using popular libraries like Jest and Sinon.js
  • Real-world examples to demonstrate effective mocking techniques

Why Do We Need Mocking?

Before diving into implementation, let's understand why mocking is essential in testing:

  1. Isolation: Mocks help isolate the code you're testing from its dependencies
  2. Speed: Tests run faster when they don't need to interact with real databases, APIs, or file systems
  3. Reliability: Tests become more reliable when they don't depend on external systems
  4. Control: You can simulate various scenarios, including error cases that might be difficult to trigger with real dependencies

Types of Mock Objects in JavaScript Testing

In JavaScript testing, you'll encounter several types of test doubles:

1. Spies

Spies observe and record function calls without changing their implementation.

javascript
// Example using Sinon.js
const sinon = require('sinon');

function sayHello(name) {
console.log(`Hello, ${name}!`);
}

// Create a spy on the sayHello function
const spy = sinon.spy(sayHello);

// Call the function
spy('John');

// Assert something about the call
console.log(spy.calledOnce); // Output: true
console.log(spy.calledWith('John')); // Output: true

2. Stubs

Stubs are like spies, but they replace the function's implementation.

javascript
// Example using Sinon.js
const sinon = require('sinon');
const user = {
getFullName: function() {
// Complex operation that might call a database
return 'John Doe';
}
};

// Create a stub
const stub = sinon.stub(user, 'getFullName');
stub.returns('Jane Smith');

console.log(user.getFullName()); // Output: 'Jane Smith'

3. Mocks

Mocks are more sophisticated test doubles that include pre-programmed expectations.

javascript
// Example using Sinon.js
const sinon = require('sinon');

const user = {
save: function(callback) {
// Save user to database
callback(null, true);
}
};

// Create a mock
const mock = sinon.mock(user);

// Set expectations
mock.expects('save').once().withArgs(sinon.match.func);

// Call the function
user.save(() => {});

// Verify expectations
mock.verify(); // Passes if all expectations were met

4. Fake Timers

Fake timers allow you to control time in your tests, which is useful for testing functions that involve setTimeout, setInterval, etc.

javascript
// Example using Jest
jest.useFakeTimers();

function delayedCallback(callback) {
setTimeout(callback, 1000);
}

test('calls the callback after 1 second', () => {
const callback = jest.fn();
delayedCallback(callback);

// At this point, the callback should not have been called yet
expect(callback).not.toBeCalled();

// Fast-forward time by 1 second
jest.advanceTimersByTime(1000);

// Now the callback should have been called
expect(callback).toBeCalled();
});

Mocking with Jest

Jest is a popular JavaScript testing framework that includes built-in mocking functionality.

Jest Mock Functions

Jest provides jest.fn() for creating mock functions:

javascript
test('mocking a simple function', () => {
// Create a mock function
const mockFn = jest.fn();

// Configure the mock to return specific values
mockFn.mockReturnValue('default');
mockFn.mockReturnValueOnce('first call');
mockFn.mockReturnValueOnce('second call');

// Call the mock function
expect(mockFn()).toBe('first call');
expect(mockFn()).toBe('second call');
expect(mockFn()).toBe('default');

// Check how many times the function was called
expect(mockFn.mock.calls.length).toBe(3);
});

Mocking Modules with Jest

Jest can mock entire modules with jest.mock():

javascript
// file: fetchData.js
import axios from 'axios';

export function fetchUserData(userId) {
return axios.get(`/api/users/${userId}`)
.then(response => response.data);
}
javascript
// file: fetchData.test.js
import axios from 'axios';
import { fetchUserData } from './fetchData';

// Mock the axios module
jest.mock('axios');

test('fetchUserData calls axios and returns data', async () => {
// Setup the mock response
const mockUser = { id: 1, name: 'John' };
axios.get.mockResolvedValue({ data: mockUser });

// Call the function
const user = await fetchUserData(1);

// Assert that the function returns the expected data
expect(user).toEqual(mockUser);

// Assert that axios.get was called with the right URL
expect(axios.get).toHaveBeenCalledWith('/api/users/1');
});

Spying on Methods

To spy on an object's method with Jest:

javascript
const calculator = {
add: (a, b) => a + b
};

test('spying on calculator.add', () => {
// Create a spy on the add method
jest.spyOn(calculator, 'add');

// Call the method
const result = calculator.add(2, 3);

// Verify the result
expect(result).toBe(5);

// Verify the method was called with the right arguments
expect(calculator.add).toHaveBeenCalledWith(2, 3);
});

Mocking with Sinon.js

Sinon.js is a dedicated mocking library that works with any testing framework. It offers more specialized features for creating spies, stubs, and mocks.

Basic Sinon.js Stub Example

javascript
const sinon = require('sinon');
const assert = require('assert');

// The function we want to test
function greetUser(userId, userService) {
const user = userService.getUserById(userId);
return `Hello, ${user.name}!`;
}

// Test with a stub
describe('greetUser', () => {
it('should greet the user by name', () => {
// Create a stub for userService
const userService = {
getUserById: sinon.stub()
};

// Configure the stub to return a specific user
userService.getUserById.withArgs(42).returns({ id: 42, name: 'Jane' });

// Call the function with our stub
const greeting = greetUser(42, userService);

// Verify the result
assert.strictEqual(greeting, 'Hello, Jane!');

// Verify the stub was called with the right arguments
sinon.assert.calledWith(userService.getUserById, 42);
});
});

Mocking HTTP Requests with Sinon.js

javascript
const sinon = require('sinon');
const axios = require('axios');

// The function we want to test
async function fetchUserProfile(userId) {
try {
const response = await axios.get(`/api/users/${userId}`);
return response.data;
} catch (error) {
return { error: 'Failed to fetch user' };
}
}

// Test with a stub
describe('fetchUserProfile', () => {
let axiosGetStub;

beforeEach(() => {
// Create a stub for axios.get
axiosGetStub = sinon.stub(axios, 'get');
});

afterEach(() => {
// Restore the original function
axiosGetStub.restore();
});

it('should return user data when the request succeeds', async () => {
// Configure the stub to resolve with data
const userData = { id: 123, name: 'Alex' };
axiosGetStub.resolves({ data: userData });

// Call the function
const result = await fetchUserProfile(123);

// Verify the result
assert.deepStrictEqual(result, userData);

// Verify the stub was called with the right URL
sinon.assert.calledWith(axiosGetStub, '/api/users/123');
});

it('should return an error object when the request fails', async () => {
// Configure the stub to reject
axiosGetStub.rejects(new Error('Network error'));

// Call the function
const result = await fetchUserProfile(123);

// Verify the result contains an error message
assert.deepStrictEqual(result, { error: 'Failed to fetch user' });
});
});

Real-world Example: Testing a Shopping Cart

Let's look at a more comprehensive example of testing a shopping cart module that depends on a product service and a discount service.

javascript
// shoppingCart.js
export class ShoppingCart {
constructor(productService, discountService) {
this.productService = productService;
this.discountService = discountService;
this.items = [];
}

addItem(productId, quantity = 1) {
const product = this.productService.getProductById(productId);
if (!product) throw new Error('Product not found');

this.items.push({
product,
quantity
});

return this.items.length;
}

getTotal() {
const subtotal = this.items.reduce((total, item) => {
return total + (item.product.price * item.quantity);
}, 0);

const discount = this.discountService.calculateDiscount(subtotal, this.items);

return subtotal - discount;
}
}

Now, let's test this shopping cart using Jest:

javascript
// shoppingCart.test.js
import { ShoppingCart } from './shoppingCart';

describe('ShoppingCart', () => {
// Set up mock services
let productService;
let discountService;
let cart;

beforeEach(() => {
// Create mock services
productService = {
getProductById: jest.fn()
};

discountService = {
calculateDiscount: jest.fn()
};

// Create a new cart with mock services
cart = new ShoppingCart(productService, discountService);
});

describe('addItem', () => {
it('should add an item to the cart when the product exists', () => {
// Arrange: setup the mock product service
const mockProduct = { id: 1, name: 'Test Product', price: 9.99 };
productService.getProductById.mockReturnValue(mockProduct);

// Act: add the item to the cart
const itemCount = cart.addItem(1, 2);

// Assert: verify the results
expect(itemCount).toBe(1);
expect(cart.items).toEqual([
{ product: mockProduct, quantity: 2 }
]);
expect(productService.getProductById).toHaveBeenCalledWith(1);
});

it('should throw an error when the product does not exist', () => {
// Arrange: setup the mock to return null (product not found)
productService.getProductById.mockReturnValue(null);

// Act & Assert: verify that adding a non-existent product throws an error
expect(() => cart.addItem(999)).toThrow('Product not found');
});
});

describe('getTotal', () => {
it('should calculate the total price with discount', () => {
// Arrange: setup mock products and discount
const product1 = { id: 1, name: 'Product 1', price: 10.00 };
const product2 = { id: 2, name: 'Product 2', price: 15.00 };

// Add items to cart (without calling addItem to avoid dependencies)
cart.items = [
{ product: product1, quantity: 2 },
{ product: product2, quantity: 1 }
];

// Mock the discount calculation to return 5.00
discountService.calculateDiscount.mockReturnValue(5.00);

// Act: calculate the total
const total = cart.getTotal();

// Assert: verify the total is calculated correctly
// Subtotal should be (10.00 * 2) + (15.00 * 1) = 35.00
// Discount is mocked to be 5.00
// Total should be 35.00 - 5.00 = 30.00
expect(total).toBe(30.00);

// Verify discount service was called with the right arguments
expect(discountService.calculateDiscount).toHaveBeenCalledWith(
35.00,
cart.items
);
});
});
});

Best Practices for JavaScript Mocking

When implementing mocks in your tests, follow these best practices:

  1. Only mock what's necessary: Mock only the dependencies you need to control, not the code you're testing
  2. Reset mocks between tests: Ensure each test starts with a clean state
  3. Be explicit about return values: Always specify what your mocks should return
  4. Verify mock interactions: Check that mocks were called with the expected arguments
  5. Don't overuse mocking: Sometimes integration tests with real dependencies are more valuable
  6. Keep mocks simple: Don't implement complex logic in mocks

Common Pitfalls to Avoid

  • Mocking everything: This makes tests unrealistic and can hide real issues
  • Not cleaning up after tests: Always restore mocks to avoid affecting other tests
  • Testing implementation details: Focus on testing behavior, not how it's implemented
  • Creating brittle tests: Don't make tests too dependent on internal implementation details

Summary

Mocking is an essential technique in JavaScript testing that allows you to isolate components, control dependencies, and test specific scenarios. In this guide, we've covered:

  • The concept of mocking and why it's important
  • Different types of test doubles (spies, stubs, mocks)
  • How to implement mocking with Jest and Sinon.js
  • A real-world example with a shopping cart
  • Best practices and pitfalls to avoid

By effectively using mocks in your tests, you can create more reliable, faster, and maintainable test suites. Remember that while mocking is powerful, it should be used judiciously to ensure your tests provide real confidence in your code.

Additional Resources and Exercises

Resources

Exercises

  1. Basic Mocking: Create a function that calls an API and write tests for it using mocks to simulate both successful and failed API responses.

  2. Time-Based Testing: Write a function that uses setTimeout to execute a callback after a delay, then test it using Jest's fake timers.

  3. Module Mocking: Create a module with multiple exports and write tests that mock specific functions from the module.

  4. Advanced Challenge: Create a simple authentication system that verifies user credentials against a database service. Write comprehensive tests using mocks to cover various scenarios like valid credentials, invalid credentials, and database errors.



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