Using Test Doubles in Jest
36 min read·Jan 1, 2025
In software testing, test doubles are objects or components that are used to simulate the behavior of real objects in a controlled way.
They "stand in" for the actual components during testing, allowing testers to isolate specific parts of the software under test and to ensure that tests are not affected by external dependencies, such as databases, networks, or third-party services.
This makes it easier to write and run tests in environments where using the real objects would be difficult, slow, expensive, or unreliable.
Fakes
A fake is an object or a function used to simulate a system or component that is not yet developed.
While it has a working implementation, it is often very simplified and not at all suitable for production use.
Example
In this example, the fakeDb object is a fake database handler, where the getUser() method is used to simulate a read operation on a database by returning a Promise containing either a fake user object or a null value if the provided email/password pair doesn't match the hard-coded one.
const fakeDb = {
data: {
users: [
{
id: 1,
email: 'user@mail.com',
password: 'helloworld'
}
]
},
getUser: function(email) {
const user = this.data.users.find(user => email === user.email);
return Promise.resolve(user || null);
}
};
it('should return a fulfilled Promise containing a user object', async () => {
// Given
const email = 'user@mail.com';
const user = {
id: 1,
email: 'user@mail.com',
password: 'helloworld'
};
// When
const result = await fakeDb.getUser(email);
// Then
expect(result).toEqual(user);
});
it('should return a fulfilled Promise containing the null value', async () => {
// Given
const email = 'user2@mail.com';
const user = null;
// When
const result = await fakeDb.getUser(email);
// Then
expect(result).toEqual(user);
});
Stubs
A stub is a function that provides a predefined (or static) response when invoked, regardless of the parameters it is invoked with.
In Jest, stubs are declared using the jest.fn() method, and their return value is set using the mockReturnValue() method:
const stub = jest.fn().mockReturnValue(value);
This means that whenever invoked, the stub() function will always return the same value.
Example
In this example, the fetch() function takes as argument a database handler and returns a function that takes as argument a user ID:
const fetch = (db) => (id) => {
return db.user.fetch(id);
}
it('should return a user object', () => {
// Given
const db = {
user: {
fetch: jest.fn().mockReturnValue({ id: 1, name: 'John Doe' })
}
};
const expected = { id: 1, name: 'John Doe' };
// When
const fetchUser = fetch(db);
const user1 = fetchUser(1);
const user2 = fetchUser(2);
// Then
expect(user1).toEqual(expected);
expect(user2).toEqual(expected);
});
Returning a different value on each call
To return a different value for each stub invocation, you chain the mockReturnValueOnce() method multiple times:
const stub = jest.fn()
.mockReturnValueOnce(value)
.mockReturnValueOnce(value);
Note: By default, if the stub function is invoked more times than the
mockReturnValueOnce()is defined, it will return anundefinedvalue.
On the other hand, to set a default value, you can combine the mockReturnValue() and mockReturnValueOnce() methods:
const stub = jest.fn()
.mockReturnValue(value)
.mockReturnValueOnce(value);
Example
In this example, the fetchUser() function will return an object on the first call and a default null value on all the following calls:
const fetch = (db) => (id) => {
return db.user.fetch(id);
}
it('should return a user object', () => {
// Given
const db = {
user: {
fetch: jest.fn()
.mockReturnValue(null)
.mockReturnValueOnce({ id: 1, name: 'John Doe' })
}
};
const user = { id: 1, name: 'John Doe' };
// When
const fetchUser = fetch(db);
const result1 = fetchUser(1);
const result2 = fetchUser(2);
// Then
expect(result1).toEqual(user);
expect(result2).toBe(null);
});
Returning asynchronous values
To define a stub that returns a fulfilled Promise, you can use the mockResolvedValue() and mockResolvedValueOnce() methods:
const asyncStub = jest.fn()
.mockResolvedValue(value)
.mockResolvedValueOnce(value);
On the other hand, to define a stub that returns a rejected Promise, you can use the mockRejectedValue() and mockRejectedValueOnce() methods:
const asyncStub = jest.fn()
.mockRejectedValue(value)
.mockRejectedValueOnce(value);
Example
In this example, the fetchUser() function will return a Promise containing an object on all calls:
const fetch = (db) => (id) => {
return db.user.fetch(id);
}
it('should return a user object', async () => {
// Given
const user = {
id: 1,
name: 'John Doe'
};
const db = {
user: {
fetch: jest.fn().mockResolvedValue(user)
}
};
// When
const fetchUser = fetch(db);
const result = await fetchUser(1);
// Then
expect(result).toEqual(user);
});
Spies
A spy is a type of test double that records information about the interactions it has with the code under test, such as how many times a method was called or with what arguments.
In Jest, to spy on the method of an object, you can use the jest.spyOn() method:
const spy = jest.spyOn(object, method);
Where:
spyis an instance of thejest.fn()method.objectis an object.methodis the method of the object you want to spy on.
Asserting invocations and parameters
To assert that a method has been invoked at least once, you can use the toHaveBeenCalled() method:
expect(spy).toHaveBeenCalled();
To assert that a method has been invoked a certain amount of times, you can use the toHaveBeenCalledTimes() method:
expect(spy).toHaveBeenCalledTimes(number);
To assert that a method has been invoked with specific parameters, you can use the toHaveBeenCalledWith() method:
expect(spy).toHaveBeenCalledWith(...parameters);
Asserting the return value
To assert that a method has returned a value and not thrown an error, you can use the toHaveReturned() method:
expect(spy).toHaveReturned();
To assert that a method has returned a value a certain number of times, you can use the toHaveReturnedTimes() method:
expect(spy).toHaveReturnedTimes(number);
To assert that a method has returned with a specific value, you can use the toHaveReturnedWith() method:
expect(spy).toHaveReturnedWith(...parameters);
Example
In this example, we're spying on the fetch() method of the db.user object to assert that it has been called only once, with an id, and that it has returned a user object:
const fetch = (db) => (id) => {
return db.user.fetch(id);
}
it('should return a user object', () => {
// Given
const db = {
user: {
fetch: jest.fn().mockReturnValue({ id: 1, name: 'John Doe' })
}
};
const id = 1;
const user = { id: 1, name: 'John Doe' };
const spy = jest.spyOn(db.user, 'fetch');
// When
const fetchUser = fetch(db);
const result = fetchUser(1);
// Then
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith(id);
expect(spy).toHaveReturnedWith(user);
});
Mocks
A mock is a special type of spy that allows us to temporarily override the implementation of a single function or an entire module, to give it another behaviour.
Mocking functions
In Jest, mocks are declared using the jest.fn() method:
const mock = jest.fn(implementation?);
Where implementation is an optional function describing the mock implementation.
Note: If no implementation is provided, the mock function will return an
undefinedvalue.
Example
In this example, we're mocking the implementation of the database.user.create() method and we're checking with which parameters is has been invoked and what it returned:
const signup = (database) => async ({ email, password }) => {
const userId = await database.user.create(email, password);
return userId;
};
it('should create a new user', async () => {
// Given
const credentials = {
email: 'user@mail.com',
password: 'helloworld'
};
const userId = 1;
const database = {
user: {
create: jest.fn((email, password) => {
return Promise.resolve(userId);
})
}
};
const signupService = signup(database);
// When
const result = await signupService(credentials);
// Then
expect(database.user.create).toHaveBeenCalledWith(credentials.email, credentials.password);
expect(result).toBe(userId);
});
Mocking modules
To ensure that the unit you are testing is in complete isolation and is not affected by the actual implementation details or side effects of the module(s) it relies on, you can mock entire modules using the jest.mock() method:
// import the module
const module = require('module');
// mock the module
jest.mock('module', implementation?);
Where implementation is an optional function describing the mock implementation.
Example
Let's consider this login module:
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
module.exports = async (data, secret, getUser) => {
const user = await getUser(data.email);
if (!user) {
throw new Error('USER_NOT_FOUND');
}
const isMatching = await bcrypt.compare(data.password, user.hash);
if (!isMatching) {
throw new Error('INCORRECT_PASSWORD');
}
const token = jwt.sign({ id: user.id }, secret, { expiresIn: '1h' });
return token;
};
When executed it will:
- Retrieve a user based on the provided email address using the
getUser()function. - Check if the retrieved password hash matches the provided password using the
bcrypt.compare()method. - Generate a JSON web token using the
jwt.sign()method.
In this example, we're mocking the bcrypt and jsonwebtoken modules to prevent them from actually performing their operations:
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const login = require('./login');
jest.mock('bcrypt', () => ({
compare: jest.fn(() => true)
}));
jest.mock('jsonwebtoken', () => ({
sign: jest.fn()
}));
afterEach(() => jest.clearAllMocks());
const data = {
email: 'user@test.com',
password: 'password'
};
const user = {
id: 1,
email: 'user@test.com',
hash: 'hash'
};
const secret = 'hashstring';
const database = {
read: jest.fn(() => user)
};
test('it should call the database with the email address', async () => {
await login(data, secret, database.read);
expect(database.read).toHaveBeenCalledWith(data.email);
});
test('it should compare the password and the hash', async () => {
await login(data, secret, database.read);
expect(bcrypt.compare).toHaveBeenCalledWith(data.password, user.hash);
});
test('it should sign the token', async () => {
await login(data, secret, database.read);
expect(jwt.sign).toHaveBeenCalledWith({ id: user.id }, secret, { expiresIn: '1h' });
});
Clearing, reseting, and restoring mocks
Each mock instance created using the jest.fn() method has the following properties:
mock.calls: an array containing the call arguments of all calls that have been made to the mock function.mock.results: an array containing the results of all calls that have been made to the mock function.mock.instances: an array containing the object instances that have been instantiated from the mock function using thenewkeyword.mock.contexts: an array containing the contexts for all calls of the mock function.
To clean up the usage data of a mock stored in the aforementioned properties between two assertions, you can use the mockClear() of the mock instance:
mockFn.mockClear();
To clear a mock and also replace its implementation with an empty function, you can use the mockReset() of the mock instance:
mockFn.mockReset()
To clear, reset, and restore the original (non-mocked) implementation, you can use the mockRestore() of the mock instance:
mockFn.mockRestore()
Note: This method only works when the mock was created with
jest.spyOn().
Alternatively, to clear, reset, or restore all mocks at once, you can use the following methods of the jest object directory:
jest.clearAllMocks();
jest.resetAllMocks();
jest.restoreAllMocks();
Summary
Here's a summary of what you've learned in this lesson:
- A fake is an object or a function used to simulate a system or component that is not yet developed.
- A stub is a function that provides a predefined (or static) response when invoked, regardless of the parameters it is invoked with.
- A spy is a type of test double that records information about the interactions it has with the code under test.
- A mock is a special type of spy that allows us to temporarily override the implementation of a single function or an entire module, to give it another behaviour.
- The
jest.fn()method is used to create stubs and mocks. - The
jest.spyOn()method is used to create spies. - The
mockReturnValue()andmockReturnValueOnce()methods are used to return a specific value for each stub invocation. - The
mockResolvedValue()andmockResolvedValueOnce()methods are used to return a resolved Promise for each stub invocation. - The
mockRejectedValue()andmockRejectedValueOnce()methods are used to return a rejected Promise for each stub invocation. - The
toHaveBeenCalled(),toHaveBeenCalledTimes(),toHaveBeenCalledWith()methods are used to check how stubs, spies, and mocks are invoked. - The
toHaveReturned(),toHaveReturnedTimes(),toHaveReturnedWith()methods are used to check what stubs, spies, and mocks have returned. - The
mockClear(),mockReset(), andmockRestore()methods are used to reset temporary data and implementation of stubs, spies, and mocks.
Enjoying the courses?
I've made these courses completely free so anyone can learn from them. If they've helped you and you'd like to actively support the work behind BackendBrewery, you can leave a tip:
Support BackendBrewery