A spy watches an existing function without necessarily replacing its behavior. This lesson covers jest.spyOn(), how it differs from jest.fn(), and how to restore the original implementation afterward.
jest.spyOn() vs jest.fn()
jest.fn() creates a mock function from nothing. jest.spyOn(object, 'methodName') instead wraps an *existing* method on a real object, letting the real implementation run by default while still tracking every call — or you can override the behavior just like a regular mock.
This makes spies ideal when you want to observe how your own code calls a method (like console.error or a class method) without necessarily changing what that method actually does.
const calculator = {
add(a, b) { return a + b; },
};
test('spies on a method call without changing behavior', () => {
const spy = jest.spyOn(calculator, 'add');
const result = calculator.add(2, 3);
expect(result).toBe(5); // real implementation still ran
expect(spy).toHaveBeenCalledWith(2, 3); // and the call was tracked
});
By default, jest.spyOn() still calls through to the real method, unlike jest.fn(), which starts with no implementation at all.
Overriding and Restoring a Spy
const spy = jest.spyOn(object, 'method');
spy.mockReturnValue(42); // override the behavior like any mock
spy.mockRestore(); // restore the original implementation
By default, a spy calls through to the real method (unless overridden).
.mockImplementation()/.mockReturnValue() on a spy override the real method's behavior.
.mockRestore() puts the original, un-spied method back in place.
restoreMocks: true in Jest config automatically restores all spies after every test.
Jest Spies Cheat Sheet
How spying differs from plain mocking, at a glance.
A very common use of jest.spyOn() is silencing and asserting on console output during a test — for example, confirming a deprecated function logs a warning, without cluttering the actual test output.
test('warns when using a deprecated option', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
configure({ oldOption: true });
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('deprecated'));
errorSpy.mockRestore();
});
Overriding with .mockImplementation(() => {}) prevents the warning from actually printing during the test run.
Spying on Class Methods
Spies work on class instance methods too, which is useful for verifying that one method calls another internally, without mocking the entire class.
class OrderService {
createOrder(items) {
this.validate(items);
return { items, total: items.length * 10 };
}
validate(items) {
if (!items.length) throw new Error('Empty order');
}
}
test('createOrder calls validate', () => {
const service = new OrderService();
const validateSpy = jest.spyOn(service, 'validate');
service.createOrder(['item-1']);
expect(validateSpy).toHaveBeenCalledWith(['item-1']);
});
Common Mistakes
Forgetting spies call through to the real implementation by default, causing unexpected side effects.
Not calling .mockRestore() (or configuring restoreMocks: true), leaking a spy's override into later tests.
Trying to jest.spyOn() a standalone function instead of a method on an object (it requires an object and a key).
Overriding a spy with .mockImplementation() when you actually just wanted to observe the real behavior.
Key Takeaways
jest.spyOn(obj, 'method') wraps an existing method, calling through to it by default.
Spies can be overridden with .mockReturnValue()/.mockImplementation() just like any mock.
.mockRestore() returns the original method, undoing the spy's effects.
Spies are ideal for observing internal calls (like console.error or a class method) without full mocking.
Pro Tip
Set restoreMocks: true in jest.config.js so every spy is automatically restored after each test — you'll rarely need to remember .mockRestore() manually again.
You now understand spies and how they differ from plain mocks. Next, look closer at mock return values and how to simulate more complex behaviors.