Mocking a class instance’s method in Jest is a common task, but it often trips people up because the way you mock depends on when the class is instantiated and how its methods are called.
Here’s a simple example. Imagine you have a DatabaseService and a UserService that depends on it:
// databaseService.js
export class DatabaseService {
async getUser(id) {
console.log(`Fetching user ${id} from real DB`);
// ... actual database call
return { id, name: 'Real User' };
}
}
// userService.js
import { DatabaseService } from './databaseService';
export class UserService {
constructor() {
this.dbService = new DatabaseService();
}
async getUserProfile(id) {
const user = await this.dbService.getUser(id);
return `User Profile: ${user.name}`;
}
}
Now, let’s say you want to test UserService without actually hitting a database. You want to mock DatabaseService.
Scenario 1: Mocking a dependency injected after instantiation
If DatabaseService were passed into UserService’s constructor or via a setter, you’d typically mock the class itself using jest.mock.
// userService.test.js
import { UserService } from './userService';
import { DatabaseService } from './databaseService';
// Tell Jest to mock all functions from DatabaseService
jest.mock('./databaseService');
// At this point, DatabaseService is a mock constructor
const MockDatabaseService = DatabaseService as jest.Mock;
describe('UserService', () => {
// Clear mocks before each test
beforeEach(() => {
MockDatabaseService.mockClear();
});
it('should get user profile', async () => {
// Create a mock instance of DatabaseService
const mockDbInstance = {
getUser: jest.fn(),
};
// Tell the mock constructor to return our mock instance
MockDatabaseService.mockReturnValue(mockDbInstance);
const userService = new UserService(); // This will now use the mock instance
// Configure the mock method on the mock instance
mockDbInstance.getUser.mockResolvedValue({ id: 1, name: 'Mocked User' });
const profile = await userService.getUserProfile(1);
expect(profile).toBe('User Profile: Mocked User');
expect(mockDbInstance.getUser).toHaveBeenCalledWith(1);
expect(MockDatabaseService).toHaveBeenCalledTimes(1); // Ensure the constructor was called
});
});
In this setup, jest.mock('./databaseService') replaces the actual DatabaseService class with a Jest mock function. MockDatabaseService.mockReturnValue(mockDbInstance) ensures that whenever new DatabaseService() is called (which happens inside UserService’s constructor), it returns our pre-configured mockDbInstance. Then, mockDbInstance.getUser.mockResolvedValue(...) sets up the specific method behavior.
Scenario 2: Mocking a dependency instantiated inside the class
This is the case in our example userService.js where this.dbService = new DatabaseService(); happens inside the constructor. The above approach works perfectly here.
Scenario 3: Mocking a method on an existing instance
Sometimes, you have a reference to a real instance of a class and you want to mock just one method on that specific instance, without replacing the whole class or its constructor. This is common when dealing with singletons or instances managed by a framework.
Let’s say UserService looked like this, and dbService was a singleton passed in:
// userService.js (modified for singleton injection)
import { DatabaseService } from './databaseService';
// Assume DatabaseService is a singleton instance elsewhere
let dbServiceInstance = new DatabaseService(); // In a real app, this might be imported from a singleton module
export class UserService {
constructor(dbService = dbServiceInstance) { // Dependency injection
this.dbService = dbService;
}
async getUserProfile(id) {
const user = await this.dbService.getUser(id);
return `User Profile: ${user.name}`;
}
}
And the test:
// userService.test.js (for singleton scenario)
import { UserService } from './userService';
import { DatabaseService } from './databaseService';
// We still mock the module to get access to the class,
// but we won't necessarily mock the constructor's return value.
jest.mock('./databaseService');
// Get the mock constructor
const MockDatabaseService = DatabaseService as jest.Mock;
describe('UserService with singleton', () => {
let mockDbInstance;
let userService;
beforeEach(() => {
// Create a *new* mock instance for each test
mockDbInstance = {
getUser: jest.fn(),
};
// Crucially, we don't use mockReturnValue here to replace the singleton.
// Instead, we inject our mock instance when creating UserService.
// If the singleton was truly managed externally, you might need to
// mock its getter or initialization logic.
userService = new UserService(mockDbInstance);
});
it('should get user profile with mocked DB method', async () => {
mockDbInstance.getUser.mockResolvedValue({ id: 2, name: 'Injected Mock' });
const profile = await userService.getUserProfile(2);
expect(profile).toBe('User Profile: Injected Mock');
expect(mockDbInstance.getUser).toHaveBeenCalledWith(2);
});
});
The jest.spyOn approach
If you have an actual instance and want to mock one of its methods temporarily for a test, jest.spyOn is your friend. It wraps an existing method, allowing you to mock its implementation and track calls, while still falling back to the original implementation if the mock is removed.
// userService.test.js (using spyOn)
import { UserService } from './userService';
import { DatabaseService } from './databaseService';
// We don't necessarily need to mock the module if we are going to
// create a real instance and spy on it. However, for isolation,
// mocking the module is often still a good idea.
jest.mock('./databaseService');
describe('UserService with spyOn', () => {
let dbServiceInstance;
let userService;
let getUserSpy;
beforeEach(() => {
// Create a REAL instance of DatabaseService (or whatever the module exports)
// If DatabaseService had complex constructor logic, you might mock it
// as in Scenario 1 and then use that mock instance.
dbServiceInstance = new DatabaseService();
userService = new UserService(dbServiceInstance); // Inject the real instance
// Use jest.spyOn to mock the 'getUser' method *on this specific instance*
getUserSpy = jest.spyOn(dbServiceInstance, 'getUser');
});
afterEach(() => {
// Restore the original implementation after each test
getUserSpy.mockRestore();
});
it('should get user profile using spyOn', async () => {
// Configure the mocked method
getUserSpy.mockResolvedValue({ id: 3, name: 'Spied User' });
const profile = await userService.getUserProfile(3);
expect(profile).toBe('User Profile: Spied User');
expect(getUserSpy).toHaveBeenCalledWith(3);
});
it('should use original implementation if spy is not active', async () => {
// If we didn't mock it, it would call the real DB method
// (This test assumes DatabaseService has a default behavior)
// For demonstration, let's ensure it's not mocked here.
getUserSpy.mockRestore(); // Remove the mock for this specific test case
// This would hit the actual database if not for jest.mock above
// which replaces the module. If DatabaseService wasn't mocked at module level,
// this would execute the real method.
const user = await dbServiceInstance.getUser(4);
expect(user).toEqual({ id: 4, name: 'Real User' }); // Assuming the real method returns this
});
});
The key difference with spyOn is that it operates on an existing object instance. jest.mock replaces the entire module or class, often affecting how instances are created. Use spyOn when you want fine-grained control over a method on a specific, live object.
When you encounter issues mocking class methods in Jest, always ask:
- Is the dependency mocked at the module level (
jest.mock)? - Is the mock configured to return a specific instance (
mockReturnValue)? - Is the method being mocked on the correct instance (
jest.spyOn)? - Are mocks being cleared between tests (
beforeEach,afterEach)?
Mastering these distinctions will help you effectively isolate your code for robust testing.