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
Jest and Vitest have a very powerful feature which lets you easily mock imports.
Basically, anything you import or require can be mocked in Jest and Vitest.
import X from 'some-module'
- either a file in your code, an external module, or a built-in.- or
require('some-module')
- again it can be a file path, external module, or something built into your JS runtime.
Example:
// vi.mock or jest.mock both support this factory signature
vi.mock('./userService', () => {
// use `default` to handle `import DefaultExport from './someService'`
const someDefaultExportObject = {
example: 'some default export',
};
return {
// named exports:
fetchUser: () => ({ name: 'Bart' }),
saveUser: () =>
'Successfully saved user',
// default export (if the module has one):
default: someDefaultExportObject,
};
});
In this lesson we'll dive into how it works, how to use it, and common issues you will see.
Basic usage
Let's start with some simple examples.
Let's say you have a file you are testing called MyWeatherService
.
In this file, if it has an import like:
import { WeatherAPI } from 'some-npm-package';
You might not want to run the real WeatherAPI
code (it'll probably do real fetch calls, require an API key, and might be slow).
So instead we can just mock that module!
// or jest.mock(...)
vi.mock('some-npm-package', () => {
class MockWeatherApi {
getWeather(city) {
return `Always sunny in ${city}`;
}
}
// Export the same named symbol that the SUT imports:
return { WeatherAPI: MockWeatherApi };
// note: if it isn't a named export (i.e. a default export)
// then return { default: MockWeatherApi }
});
Syntax
You pass in 2 arguments:
- first is the module name. this can be a relative file path btw
- second (optional) is a function which returns an object with all the exports from that module.
Automatic Mocking
In the examples so far we have always provided the mocked module's exports.
But the simplest way to use jest.mock()
or vi.mock()
is to just provide a module name/file path.
Any exports will be 'empty' - functions will be jest.fn()
(or vi.fn()
), and any exported constants will be undefined
.
// ./error-check.js (this is the file we'll mock)
export const checkValueIsError =
value => {
return value.length > 1;
};
export const DEFAULT_GREETING =
'Hi there';
// someService.js
import {
checkValueIsError,
DEFAULT_GREETING,
} from './error-check';
export function sayHi(
name,
someValue,
useDefaultGreeting = false
) {
if (checkValueIsError(someValue)) {
return 'No greeting for you!';
}
if (useDefaultGreeting) {
return (
DEFAULT_GREETING + ' ' + name
);
}
return 'Hi, ' + name;
}
export const someExportedConst =
'HELLO';
// someService.test.ts (your test)
import { sayHi } from './someService';
// mock it with automatic mocking
vi.mock('./error-check'); // or jest.mock('./error-check')
test('it runs ok', () => {
expect(
sayHi('Lisa', '1234', false)
).toBe('Hi, Lisa');
});
What happens in this case is that all exports from that module (./error-check
) get fake versions and no real implementation code is run for it.
This means the exported constant of DEFAULT_GREETING is undefined, so this would fail:
expect(
sayHi('Lisa', 'some value', true)
).toBe('Hi there Lisa');
because DEFAULT_GREETING
would be undefined.
Basically our error-check.js
file is now exporting something like this:
// all exported functions become jest.fn() or vi.fn()
export const checkValueIsError =
vi.fn(); // or jest.fn()
// all exported constants become undefined
export const DEFAULT_GREETING =
undefined;
Partial mocking
When you mock a module, the entire module is mocked. (If you want to mock just one export, you can normally use vi.spyOn
or jest.spyOn
- see other lesson for this)
Sometimes you want the real implementation for most of the mocked module - and you can get this by using jest.requireActual()
or vi.importActual()
like this:
// Example with vitest (importActual)
vi.mock('./userService', async () => ({
...(await vi.importActual(
'./userService'
)),
fetchUser: vi.fn(),
}));
// Example with jest (requireActual)
jest.mock('./userService', () => ({
...jest.requireActual(
'./userService'
),
fetchUser: jest.fn(),
}));
Hoisting
Mocks are hoisted to the top of the file, before any imports. It has to do this so it can run your code to set up the mocked modules, otherwise your test might import something before the mock is applied.
This makes using jest.mock()
or vi.mock()
complex.
The following does not work for example
import { WeatherDisplay } from './WeatherDisplay';
import * as weatherService from './weatherService';
// This variable won't be accessible in the mock below
const mockTemperature = '75°F';
// ❌ This will throw a ReferenceError!
vi.mock('./weatherService', () => ({
// or jest.mock(...)
getCurrentWeather: vi
.fn() // or jest.fn()
.mockResolvedValue(
`${mockTemperature} in Seattle`
),
}));
It fails because the mock is hoisted above the mockTemperature declaration, so it doesn't exist yet.
To work around this there are a couple of ways:
Put all variables in the mock factory function
The 2nd argument to mock() is just a plain function (it just can't reference things outside of itself). So you can define mock values in there.
// or jest.mock(...)
vi.mock('./weatherService', () => ({
getCurrentWeather: vi
.fn() // or jest.fn()
.mockResolvedValue(
'35°C in Philadelphia'
),
API_BASE_URL: 'https://mock-api.com',
DEFAULT_TIMEOUT: 1000,
}));
Use doMock
jest.doMock()
or vi.doMock()
works in a very similar way to jest.mock()
or vi.mock()
, but it does not get hoisted.
When to use doMock
Use doMock when you need to:
- Create different mocks per test, which behave differently
- Use runtime variables in your mocks (that wouldn't be available when mock() is hoisted)
- Conditionally mock modules
describe('WeatherService', () => {
afterEach(() => {
vi.resetModules(); // Important: Clear module cache between tests
// or jest.resetModules()
});
test('handles sunny weather', async () => {
// This test needs the API to return sunny weather
// or jest.doMock
vi.doMock('./weatherAPI', () => ({
getCurrentWeather: vi
.fn() // or jest.fn()
.mockResolvedValue({
condition: 'sunny',
temperature: 75,
}),
}));
// Import AFTER doMock
const { WeatherService } =
await import('./weatherService');
const service =
new WeatherService();
const result =
await service.getWeatherAdvice();
expect(result).toBe(
'Great day for a picnic!'
);
});
test('handles rainy weather', async () => {
// This test needs the API to return rainy weather
// or jest.doMock(...) (with jest.fn())
vi.doMock('./weatherAPI', () => {
return {
getCurrentWeather: vi
.fn()
.mockResolvedValue({
condition: 'rainy',
temperature: 60,
}),
};
});
const { WeatherService } =
await import('./weatherService');
const service =
new WeatherService();
const result =
await service.getWeatherAdvice();
expect(result).toBe(
'Better bring an umbrella!'
);
});
});
Resetting module mocks
You can use jest.resetModules()
(or vi.resetModules()
) to reset their mocks.
This is quite useful with jest.doMock()
or vi.doMock()
afterEach(() => {
vi.resetModules(); // Clear the module cache
// (jest.resetModules() for jest)
});
Mocking default exports - CJS vs ESM
When mocking modules with default exports, the syntax is slightly different depending if the code is using CommonJS (CJS) or ES Modules (ESM).
ES Modules (ESM) - default exports:
If you have this file - math.js
:
export default function add(a, b) {
return a + b;
}
then to mock it in your tests you can do this so calls to add()
will always return 12. Note you have to use the mock factory, returning an object with the default
key:
// or jest.mock(...)
vi.mock('./math', () => ({
// In some setups you may also need `__esModule: true`
__esModule: true,
default: vi.fn(() => 12), // mock the default export
}));
CommonJS (CJS) - default exports:
If you have this file (math.js) which uses CJS exports via module.exports
:
function add(a, b) {
return a + b;
}
module.exports = add;
Then to mock it in your tests you simply return the mock function directly in your mock factory (no need to put it on an object with default
as a property key):
jest.mock('./math', () =>
jest.fn(() => 12)
);
*Why is __esModule: true
needed?
__esModule: true
tells the test runner to treat the mock as an ES module so that the default
property is exposed as the module's default export.
Without it (depending on your transformer/config) when you try to import a default export, it might either return undefined
(or the default export is only accessed via .default
Internally, if you use the babel transpiler, it will convert ES Modules to CJS and adds __esModule: true
. The Typescript compiler will also make use of this before Jest executes the code.
Summary/tip: If you get undefined
when importing a default export from your mock, add __esModule: true
to the object that the mock factory returns.
Real world example of mocking Axios
In this example we have a UserService
which uses axios
to fetch API calls.
I haven't shown the UserService implementation as it isn't important here. The important part is how we are mocking axios
.
import axios from 'axios';
import { UserService } from './userService';
// Mock the entire axios module
// we are using automatic mocking
// but then in our test we set mock return data.
jest.mock('axios');
// If you are using Typescript, it is easier to type the
// mock like this:
const mockAxios = axios as jest.Mocked<
typeof axios
>;
describe('UserService', () => {
let userService: UserService;
beforeEach(() => {
userService = new UserService();
jest.clearAllMocks();
});
describe('getUser', () => {
test('should return user data when API call succeeds', async () => {
const userId = 1;
const mockUser = {
id: 1,
name: 'Bob',
email: 'bob@example.com',
};
// Configure the mock to return a successful response.
// We import axios and narrow it to `jest.Mocked<typeof axios>` (TypeScript),
// which exposes typed mock methods. Under the hood it's a mocked module,
// (which means axios.get === jest.fn() or vi.fn())
// so we can set return values directly on `mockAxios.get`.
mockAxios.get.mockResolvedValue({
data: mockUser,
status: 200,
statusText: 'OK',
});
const result =
await userService.getUser(
userId
);
expect(result).toEqual(mockUser);
expect(
mockAxios.get
).toHaveBeenCalledWith(
'https://api.example.com/users/1'
);
expect(
mockAxios.get
).toHaveBeenCalledTimes(1);
});
test('should throw error when API call fails', async () => {
const userId = 1;
const errorMessage =
'User not found';
mockAxios.get.mockRejectedValue(
new Error(errorMessage)
);
await expect(
userService.getUser(userId)
).rejects.toThrow(errorMessage);
expect(
mockAxios.get
).toHaveBeenCalledWith(
'https://api.example.com/users/1'
);
});
});
});
Optional reading: How it works
Sometimes it can be useful to understand how things work even if it isn't really required to understand it to use it.
On a very high level, when you run a test file in Jest or Vitest, it builds up a fake environment. Every import (import X from 'Y'
or require('X')
) is handled by a fake import system. You can think of it a bit like it creates one single JS file, which all the imports copy/pasted into it.
note: This is a highly simplified explanation
Because it is fully controlling all of the imports, we can do some magic in Jest and Vitest like rewrite what an imported module contains!
mock() is not supported in our in-browser test runner
Mocking is complex and we cannot fully support it in a web environment.
You can play around with it with a very specific module to mock just to play with it.
The editor is configured to mock the example-module-to-mock
module so you can see how it works.
You cannot mock anything other than example-module-to-mock
in our in-browser editor.
I recommend trying mocking on your own Jest or Vitest runner.
Lesson Task
Due to the limitations on mocking in a test simulator in a browser environment, this is not really supported here. You can have a play with the code & run it (it will pass), but our simulator will only mock example-module-to-mock
.
Read the code, understand it - but to truly play with it please run it locally
Ready to try running tests for this lesson?