Mocking and Spies (with Sinon.js or Jest)
In software development, especially when writing unit tests, it's crucial to be able to isolate the component being tested from its external dependencies. This ensures that the test truly measures the behavior of a unit and is not affected by the behavior of other components or external systems (such as databases, third-party APIs, file systems, etc.). This is wheremocking and spies (also known as stubs or fakes) come into play. They are powerful tools that allow us to simulate the behavior of these dependencies and verify how the code interacts with them.
What are Mocking, Spies, and Stubs?
- Spy:
A spy is a function that wraps an existing function or method totrack its behavior without changing its original implementation. You can use a spy to verify:
- If the function was called.
- How many times it was called.
- With what arguments it was called.
- What value it returned.
- If it threw an error.
Spies are ideal when you want to observe interactions with a dependency but still want the dependency to execute its original behavior.
- Stub:
A stub is a function that completely replaces a real function or method with a test-controlled implementation. Stubs are used to:
- Force a specific outcome (e.g., always return a specific value).
- Simulate an error being thrown.
- Prevent a real method from executing (e.g., avoid calls to a database or network).
Unlike spies, stubs change the behavior of the original function. They can also record calls just like spies.
- Mock:
A mock is a type of stub that not only replaces the function but also verifies interactions with it. Mocks are often defined with pre-programmed expectations about how they should be called. If the expectations are not met, the test fails. Mocks are used when you need to assert that a dependency was invoked in a specific way.
In the context of Jest, `jest.fn()` can act as both a spy and a stub, and by combining it with `expect` and its matchers (`toHaveBeenCalledWith`, `toHaveReturnedWith`), it behaves like a mock.
Mocking and Spies with Jest
Jest, being an "all-in-one" framework, has built-in mocking and spying capabilities through its global `jest` object.
jest.fn()
(Spies / Stubs / Mocks):
Creates a mocked function that allows you to spy on its behavior and control its return.
// src/dataService.js
const axios = require('axios');
async function fetchUser(id) {
const response = await axios.get(`https://api.example.com/users/${id}`);
return response.data;
}
function saveLog(message) {
console.log(message);
}
module.exports = { fetchUser, saveLog };
// __tests__/dataService.test.js
const dataService = require('../src/dataService');
const axios = require('axios'); // We import axios to be able to mock it
// Mock the entire axios module
jest.mock('axios');
describe('dataService with Jest mocks', () => {
let consoleSpy;
beforeEach(() => {
// Restore all mocks after each test
jest.clearAllMocks();
// Spy on console.log for the saveLog test
consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
});
afterEach(() => {
// Restore the original console.log
consoleSpy.mockRestore();
});
test('fetchUser should return user data', async () => {
const mockUserData = { id: 1, name: 'Test User' };
axios.get.mockResolvedValue({ data: mockUserData }); // Mock the return of axios.get
const user = await dataService.fetchUser(1);
expect(axios.get).toHaveBeenCalledTimes(1);
expect(axios.get).toHaveBeenCalledWith('https://api.example.com/users/1');
expect(user).toEqual(mockUserData);
});
test('saveLog should call console.log with the correct message', () => {
const message = 'Test log message';
dataService.saveLog(message);
expect(consoleSpy).toHaveBeenCalledTimes(1);
expect(consoleSpy).toHaveBeenCalledWith(message);
});
});
jest.spyOn()
:
Creates a spy on an existing method of an object. It allows you to observe the method call without changing its default implementation. You can chain `mockImplementation` or `mockReturnValue` to turn it into a temporary stub.
// Example of jest.spyOn (already included above with console.log)
Mocking and Spies with Sinon.js (and Mocha/Chai)
If you are using Mocha with Chai, Sinon.js is the most popular and robust mocking and spying library to complement your tests.
Installation:
npm install --save-dev mocha chai sinon
Test example with Sinon.js:
// src/dataService.js
const axios = require('axios');
async function fetchUser(id) {
const response = await axios.get(`https://api.example.com/users/${id}`);
return response.data;
}
function saveLog(message) {
console.log(message);
}
module.exports = { fetchUser, saveLog };
// test/dataService.test.js
const { expect } = require('chai');
const sinon = require('sinon');
const dataService = require('../src/dataService');
const axios = require('axios'); // We import axios to be able to mock it
describe('dataService with Sinon.js', () => {
let axiosGetStub;
let consoleSpy;
beforeEach(() => {
// Restore stubs/spies after each test
sinon.restore();
// Stub axios.get to control its return
axiosGetStub = sinon.stub(axios, 'get');
// Spy on console.log to observe calls
consoleSpy = sinon.spy(console, 'log');
});
it('fetchUser should return user data', async () => {
const mockUserData = { id: 1, name: 'Test User' };
axiosGetStub.returns(Promise.resolve({ data: mockUserData })); // Stub axios.get to return a value
const user = await dataService.fetchUser(1);
expect(axiosGetStub.calledOnce).to.be.true;
expect(axiosGetStub.calledWith('https://api.example.com/users/1')).to.be.true;
expect(user).to.deep.equal(mockUserData);
});
it('saveLog should call console.log with the correct message', () => {
const message = 'Test log message';
dataService.saveLog(message);
expect(consoleSpy.calledOnce).to.be.true;
expect(consoleSpy.calledWith(message)).to.be.true;
});
});
When to use each one?
- Spies: Use them when you only need to verify if a function was called and with what arguments, but you want the original function to continue executing. They are excellent for observing side effects or interactions with other parts of the system without altering their behavior.
- Stubs: Employ them when you need to control the outcome of a dependency's function. For example, to simulate a network error, a successful database response, or a fixed value. They are useful for testing different scenarios (success, error, empty data).
- Mocks: These are used when you need to define a specific behavior for a dependency and then assert that this dependency was invoked in a predefined way. They are stricter and are often used in TDD (Test-Driven Development) to define interactions before implementing the code. In Jest, `jest.fn()` with its `expect` matchers covers most mocking cases.
Mocking and spies are indispensable techniques for writing effective and reliable unit tests in Node.js. By isolating the unit of code you are testing from its dependencies, you can ensure that your tests are fast, deterministic, and truly measure what they are intended to. Whether with Jest's built-in capabilities or Sinon.js's flexibility, mastering these tools will significantly elevate the quality of your test suite.