Jest/Vitest Testing Fundamentals
Learn the basics of how to test with Jest or Vitest. This course covers the main features for writing assertions, tests, mocks, async testing, and more
Getting Started
Common Matchers
Basic Testing Concepts
Learn the basics of how to test with Jest or Vitest. This course covers the main features for writing assertions, tests, mocks, async testing, and more
Getting Started
Common Matchers
Basic Testing Concepts
In Jest and Vitest, spyOn is one of the most useful advanced features.
(Advanced feature, but very essential to writing tests every day).
It lets you track how many times a function was called, what it was called with, and you can (optionally) replace the implementation of a function.
Basic Example of using spyOn to check if a function was called.
const userService = {
getUser: id => `User ${id}`,
};
const spy = jest.spyOn(
userService,
'getUser'
);
// or for vitest:
// vi.spyOn(userService, 'getUser')
userService.getUser('123');
expect(spy).toHaveBeenCalledWith('123');
(Note: jest.spyOn
/ vi.spyOn
still call the original implementation by default; they only replace it if you set a mock return value or implementation. See below.)
You use spyOn
on an object that contains a function.
If you do not add an implementation or a return value, then it is literally just a spy (it logs the calls, but doesn't change the real implementation)
It is more common to use spyOn along with a mock return value, which you can see below.
Note: You can import * as SomeImport from 'whatever'
, and then call vi.spyOn(SomeImport, 'fnToSpyOn')
or jest.spyOn(SomeImport, 'fnToSpyOn')
- examples below.
Simple example - track if a fn was called
class UserService {
getUser(id) {
return `User ${id}`;
}
}
test('example of spyOn - check if a fn was called', () => {
const userService = new UserService();
// (or jest.spyOn(...)
const getUserSpy = vi.spyOn(
userService,
'getUser'
);
const result =
userService.getUser('123');
expect(result).toBe('User 123'); // << note: this is still calling real implementation
expect(
getUserSpy
).toHaveBeenCalledWith('123');
});
Combining the spying with a mock return value
As you saw in the previous lesson, jest.fn()
/vi.fn()
tracks the usage, and can change the return value.
The spyOn
returns the same as jest.fn()
/ vi.fn()
, so you can still set mockReturnValue()
etc.
test('example of spyOn - check if a fn was called & set a mock response', () => {
const userService = new UserService();
const getUserSpy = vi
.spyOn(userService, 'getUser')
.mockReturnValue(
'mock user response'
);
const result =
userService.getUser('123');
expect(result).toBe(
'mock user response'
); // << using the mock return value this time
expect(
getUserSpy
).toHaveBeenCalledWith('123');
});
Restoring a mock
One of the very useful things about using spyOn is that you can restore the original implementation
class UserService {
getUser(id) {
return `Real user ${id}`;
}
}
test('example of spyOn - check if a fn was called', () => {
const userService = new UserService();
const getUserSpy = vi.spyOn(
userService,
'getUser'
);
expect(
userService.getUser('123')
).toBe('Real user 123');
getUserSpy.mockReturnValue(
'mock user'
);
expect(
userService.getUser('123')
).toBe('mock user');
expect(
getUserSpy
).toHaveBeenCalledTimes(2);
getUserSpy.mockRestore(); // puts the original implementation back
expect(
userService.getUser('123')
).toBe('Real user 123');
});
Spying on a module import
Sometimes you need to spy on functions from imported modules.
You can do this by importing the entire module with import * as
syntax:
// userUtils.js
export function formatUserName(
firstName,
lastName
) {
return `${firstName} ${lastName}`;
}
export function validateEmail(email) {
return email.includes('@');
}
then in your test:
// userService.test.js
import * as UserUtils from './userUtils'; // << this is important
test('spying on module exports', () => {
// or jest.spyOn(UserUtils, 'formatUserName');
const formatSpy = vi.spyOn(
UserUtils,
'formatUserName'
);
formatSpy.mockReturnValue(
'MOCKED NAME'
);
const result =
UserUtils.formatUserName(
'Bob',
'Bobby'
);
expect(result).toBe('MOCKED NAME');
expect(
formatSpy
).toHaveBeenCalledWith(
'Bob',
'Bobby'
);
// Restore original implementation
formatSpy.mockRestore();
expect(
UserUtils.formatUserName(
'Bob',
'Bobby'
)
).toBe('Bob Bobby');
});
You can also spy on multiple functions from the same module:
test('spying on multiple module exports', () => {
const formatSpy = vi.spyOn(
UserUtils,
'formatUserName'
);
// or jest.spyOn(...)
const validateSpy = vi.spyOn(
UserUtils,
'validateEmail'
);
formatSpy.mockReturnValue(
'Mock User'
);
validateSpy.mockReturnValue(true);
expect(
UserUtils.formatUserName('a', 'b')
).toBe('Mock User');
expect(
UserUtils.validateEmail('invalid')
).toBe(true);
expect(
formatSpy
).toHaveBeenCalledTimes(1);
expect(
validateSpy
).toHaveBeenCalledTimes(1);
});
Lesson Task
In the spy on a simple object
tests you need to spyOn someObject
's getAge
method. You can use vi.spyOn(someObject, 'getAge')
. Then you use .mockReturnValue(99)
to set what value the function returns when called.
For the second test, we want to spy on an imported module. Most of this test is written so it passes, but you have to check it was called and set the mock response
Ready to try running tests for this lesson?