Jest
1. Installation
Add a script:
{
"scripts": {
"test": "jest"
}
}
Install these:
pnpm add -D jest jest-environment-jsdom ts-jest json-easy-strip @types/jest @testing-library/dom @testing-library/jest-dom @testing-library/react @testing-library/user-event
Create a jest.config.js
file:
(this file was generated using jest --init
)
import requireJSON from 'json-easy-strip';
import { createDefaultPreset } from 'ts-jest';
import { pathsToModuleNameMapper } from 'ts-jest';
const tsconfig = requireJSON('./tsconfig.json');
/**
* @type {import('ts-jest').JestConfigWithTsJest}
*/
const config = {
...createDefaultPreset(),
// A preset that is used as a base for Jest's configuration
preset: 'ts-jest',
// The test environment that will be used for testing
testEnvironment: 'jsdom', // <--- IMPORTANT! must be 'jsdom'. Option are: 'jsdom' | 'node'.
// A list of paths to modules that run some code to configure or set up the testing framework before each test
setupFilesAfterEnv: ['<rootDir>/src/tests/setup.tsx'],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
moduleNameMapper: pathsToModuleNameMapper(tsconfig.compilerOptions.paths, {
prefix: '<rootDir>/',
}),
// A map from regular expressions to paths to transformers
transform: {
'^.+.tsx?$': ['ts-jest', {}],
},
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/private/var/folders/yv/tfvrv22n3rn8g7_2dzx2n8lc0000gn/T/jest_dx",
// Automatically clear mock calls, instances, contexts and results before every test
// clearMocks: false,
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: false,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
// coverageDirectory: undefined,
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
// coverageProvider: "babel",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// The default configuration for fake timers
// fakeTimers: {
// "enableGlobally": false
// },
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "mjs",
// "cjs",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state before every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};
export default config;
Create a setup.tsx
file:
// --- Write here any Code you need as a setup! ---
Import css/scss modules
Running a Component Test on a component that imports a css/scss module file would fail.
You'll see an error like so:
● Test suite failed to run
Jest encountered an unexpected token
Jest failed to parse a file. This happens ...
1 | import clsx from 'clsx';
> 2 | import styles from './Textarea.module.scss';
| ^
To solve that, you'll need to configure the moduleNameMapper
option in your jest.config.js
file.
const config = {
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
moduleNameMapper: {
...pathsToModuleNameMapper(tsconfig.compilerOptions.paths, {
prefix: '<rootDir>/',
}),
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
}
identity-obj-proxy
- If you are using CSS Modules then it's better to mock a proxy to enable className lookups.
You of course need to install it as a devDependency:
pnpm add -D identity-obj-proxy
Extra
There are many ways to configure jest, we are using ts-jest
.
Other ways are:
- Babel
- webpack
- Vite (Jest is not fully supported by vite)
- Parcel
- TypeScript (through
ts-jest
)
2. Important CLI options to know
--listTests
Lists all test files that Jest will run given the arguments, and exits.
--config=<path>
Alias: -c
. The path to a Jest config file specifying how to find and execute tests. If no rootDir
is set in the config, the directory containing the config file is assumed to be the rootDir
for the project. This can also be a JSON-encoded value which Jest will use as configuration.
--showConfig
Print your Jest config and then exits.
--passWithNoTests
Allows the test suite to pass when no files are found.
--runTestsByPath
Run only the tests that were specified with their exact paths. This avoids converting them into a regular expression and matching it against every single file.
For example, given the following file structure:
__tests__
└── t1.test.js # test
└── t2.test.js # test
When ran with a pattern, no test is found:
jest --runTestsByPath __tests__/t
Output:
No tests found
However, passing an exact path will execute only the given test:
jest --runTestsByPath __tests__/t1.test.js
--silent
Prevent tests from printing messages through the console.
--verbose
Display individual test results with the test suite hierarchy.
3. Catch a thrown Error
If you want to test whether a particular function throws an error when it's called, use toThrow
.
function compileAndroidCode() {
throw new Error('you are using the wrong JDK!');
}
test('compiling android goes as expected', () => {
expect(() => compileAndroidCode()).toThrow();
expect(() => compileAndroidCode()).toThrow(Error);
// You can also use a string that must be contained in the error message or a regexp
expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK');
expect(() => compileAndroidCode()).toThrow(/JDK/);
// Or you can match an exact error message using a regexp like below
expect(() => compileAndroidCode()).toThrow(/^you are using the wrong JDK$/); // Test fails
expect(() => compileAndroidCode()).toThrow(/^you are using the wrong JDK!$/); // Test pass
});
4. Testing Asynchronous Code
Jest needs to know when the code it is testing has completed before it can move on to another test. Jest has several ways to handle this.
- Promises: Return a promise from your test, and Jest will wait for that promise to resolve. If the promise is rejected, the test will fail.
- Async/Await: Make your test
async
and use theawait
keyword inside.
These two tests are equal:
test('the data is peanut butter', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});
test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
resolves
& rejects
You can combine async and await with .resolves
or .rejects
.
For that to work you need to pass a promise to the expect
, and add an await keyword before the expect
. Jest will wait for that promise to either resolve or reject (depending)and if the promise is rejected when it should be resolved, or vice versa, the test will fail.
test('the data is peanut butter', async () => {
await expect(fetchData()).resolves.toBe('peanut butter');
});
test('the fetch fails with an error', async () => {
await expect(fetchData()).rejects.toMatch('error');
});
Be sure to return (or await
) the promise - if you omit the return
/await
statement, your test will complete before the promise returned from fetchData
resolves or rejects.
For example,
async function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
}
test('1 equals 2', () => {
fetchData().then((data) => {
expect(data).toBe(2);
});
});
The above test will PASS, but it should NOT PASS!
5. Mock Functions
Here are all the possible scenarios you might have:
- You wanna test function A. Function A accepts callback function as one of its parameters.
- You wanna test function A. Function A uses another function B. Function A imports function B from a different file/module.
- You wanna test function A. Function A uses another function B. Functions A and B both live on the same file.
- You wanna test function A. Function A uses another function B. You need to mock the returned value with different values at each time.
- You wanna test Class A. Class A has method B which returns
this
.
Scenario 1: Mock callback fn as argument using jest.fn()
A. Goal Explanation
You wanna test function A. Function A accepts callback function as one of its parameters.
B. Example code to test
export function forEach(items, callback) {
for (const item of items) {
callback(item);
}
}
C. Case Description
Above we have a function called forEach
that we want to test. forEach
accepts callback
as a parameter, ad calls it inside for each item under the items array.
Our goal is to write a test that:
- checks how many times
mockCallback
has been called. - checks the first argument of the first and second calls were as expected.
D. How to test
What are the steps to achieve this goal?
- We will use
jest.fn()
to create amockCallback
const. - We will pass
mockCallback
as the argument to our tested function (forEach). - We will inspect the
mock
property ofmockCallback
(more specifically themockCallback.mock.calls
sub-property). - Use plain matchers such as
toBe
ortoHaveLength
.
We will write the test as such:
import { forEach } from './forEach';
const mockCallback = jest.fn();
test('forEach mock function', () => {
const items = [0, 1];
forEach(items, mockCallback);
// The mock function was called al least once
expect(mockCallback.mock.calls.length).toBeGreaterThan(0);
// The mock function was called exactly twice
expect(mockCallback.mock.calls).toHaveLength(2);
// The first argument of the first call to the function was 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
// The first argument of the second call to the function was 1
expect(mockCallback.mock.calls[1][0]).toBe(1);
});
Instead of using the .mock
property directly, jest had created a syntactic sugar around it for every operation and check we might need. So we will not be writing test as above.
A better way to write it is:
import { forEach } from './forEach';
const mockCallback = jest.fn();
test('forEach mock function', () => {
const items = [0, 1];
forEach(items, mockCallback);
// The mock function was called al least once
expect(mockCallback.mock.calls.length).toBeGreaterThan(0);
expect(mockCallback).toHaveBeenCalled();
// The mock function was called exactly twice
expect(mockCallback.mock.calls).toHaveLength(2);
expect(mockCallback).toHaveBeenCalledTimes(2);
// The first argument of the first call to the function was 0
expect(mockCallback.mock.calls[0][0]).toBe(0);
expect(mockCallback).toHaveBeenNthCalledWith(1, 0);
// The first argument of the second call to the function was 1
expect(mockCallback.mock.calls[1][0]).toBe(1);
expect(mockCallback).toHaveBeenNthCalledWith(2, 1);
});
Scenario 2: Mocking Modules with jest.mock(...)
A. Goal Explanation
You wanna test function A. Function A uses another function B. Function A imports function B from a different file/module.
B. Example code to test
import axios from 'axios';
type Filters = {
nameStartsWith: string;
ageGreaterThan: number;
};
export class Users {
static async findMany(filters?: Filters) {
const updatedFilters: Filters = {
ageGreaterThan: filters?.ageGreaterThan ?? 0,
nameStartsWith: filters?.nameStartsWith ?? 't',
};
const response = await axios.get('/users.json', {
headers: {
'x-filter-name': updatedFilters.nameStartsWith,
'x-filter-age': updatedFilters.ageGreaterThan,
},
});
const { data } = response;
return data;
}
}
C. Case Description
Above we have a class called Users
with a method called findMany
which we want to test. Users
calls axios
, an imported module, under the hood to send all of its async api requests. The findMany
method specifically calls axios.get
.
Let's say that our goal is to write a test that:
- checks that
axios.get
had been called at least once. - checks that the headers (a property under the second argument) were created as expected.
- mock the response value so that the test won't crash
D. How to test
What are the steps to achieve this goal?
- We will use
jest.mock('module-name')
to specify just which module to mock. - Import the module into our test. This will give us 100% control (we'll see what it means soon).
- We will mock wanted methods on axios (i.e.
get
). - We would use matchers like
.haveBeenCalledOnce
and such on those mocked methods.
• Why we need so many things?
First, let's explain why we need so many things. Like, why we need to write jest.mock
, but also import the actual module to be mocked.
Here's a test template:
import Users from './users';
test('should fetch users', async () => {
const data = await Users.findMany();
// ???
});
We are calling Users.findMany()
, which under the hood calls axios
.
axios
needs to be mocked.
We start by adding jest.mock
:
import Users from './users';
jest.mock('axios');
// ...
What this will do is it will mock any import of the axios module, and wrap it around some jest
function.
The problem with the code above is the return value. Users.findMany()
calls axios.get()
, and expects it to return some data. If it won't return the same data structure, the code will crash. We now need a way to alter the return value of axios.get()
.
What we can do is this (not recommended):
import { Users } from './users';
jest.mock('axios', () => {
return {
get: jest.fn(() => ({ data: { age: 5 } })),
};
});
test('should fetch users', async () => {
const data = await Users.findMany();
expect(data.age).toBe(5);
});
While this works, we are still missing something.
What we wanna do is be able to ask questions like:
- How many times it has
get
been called? - What were the arguments
get
was called with?
We don't have access to the get
function (yet!). To get it, we need to import axios INSIDE our test!
import axios from 'axios';
import { Users } from './users';
console.log('axios is:', axios);
jest.mock('axios', () => {
return {
get: jest.fn(() => ({ data: { age: 5 } })),
};
});
In the above code, if we were to put breakpoints in lines 4 and 6, line 6 would be hit first! jest.mock()
is hoisted all the way to the top! Before any of the imports occur. This guarantees that the import of axios
inside our test will be of the mocked axios, and not the real axios. This now gives us the ability to ask questions about axios.get
inside of tests:
With adding the import axios
statement, the test file will look like this:
import axios from 'axios';
import { Users } from './users';
jest.mock('axios', () => {
return {
get: jest.fn(() => ({ data: { age: 5 } })),
};
});
test('should fetch users', async () => {
const data = await Users.findMany();
expect(axios.get).toHaveBeenCalled();
expect(axios.get).toHaveBeenCalledWith('/users.json', {
headers: { 'x-filter-name': 't', 'x-filter-age': 0 },
});
expect(data.age).toBe(5);
});
The above code is great, but there's one thing to note about it - the implementation mock that is provided as the second argument to jest.mock
will stay the same for every test in that file. Well, but what if we want to have flexibility over different test?
What we can do is to not provide a mock implementation as the second argument, and have each test define it on the fly for its own use.
Like so:
import axios from 'axios';
import { Users } from './users';
jest.mock('axios');
test('should fetch users', async () => {
const data = await Users.findMany();
expect(axios.get).toHaveBeenCalled();
expect(axios.get).toHaveBeenCalledWith('/users.json', {
headers: { 'x-filter-name': 't', 'x-filter-age': 0 },
});
expect(data.age).toBe(5);
});
You're probably seeing that typescript error under mockResolvedValue
. Your IDE thinks it's the real axios
, and that axios.get
has no property of mockResolvedValue
on it. We need to tell it that axios.get
is a jest.fn
type:
import axios from 'axios';
import { Users } from './users';
jest.mock('axios');
test('should fetch users', async () => {
(axios.get as jest.Mock).mockResolvedValue({ data: { age: 5 } });
// Would also work: (axios.get as jest.Mock).mockReturnValue({ data: { age: 5 } });
const data = await Users.findMany();
expect(axios.get).toHaveBeenCalled();
expect(axios.get).toHaveBeenCalledWith('/users.json', {
headers: { 'x-filter-name': 't', 'x-filter-age': 0 },
});
expect(data.age).toBe(5);
});
Scenario 3: Mocking Modules Partially
A. Goal Explanation
You wanna test function A. Function A uses another function B. Functions A and B both live on the same file.
B. Example code to test
export const namedExportVariable = 'named export variable';
export function namedExportFunction() {
return 'named export function';
};
export default () => 'export default function';
C. Case Description
Steps to achieve this goal:
- We will again use
jest.mock
- We will pass it a mock implementation of the module as the seconds argument
- Inside the mock implementation:
- We will use
jest.requireActual('path')
, which is a special jest function that allows us to import the real module. - We will return an object, where inside it:
- We will spread
jest.requireActual('path')
. Those will be all the parts of the module we want as unmocked. - Below the spread, we will override the parts we want to mock.
- We will spread
- We will use
- If you need to import the module as default, the return object of the mock implementation MUST contain the special key of
__esModule: true,
.
D. How to test
Example 1: a named export
If we only want to mock namedExportFunction
, then our test file should be:
import { namedExportFunction, namedExportVariable } from './foo-bar-baz';
jest.mock('./foo-bar-baz', () => {
const originalModule = jest.requireActual('./foo-bar-baz');
return {
...originalModule,
namedExportFunction: jest.fn(() => 'mocked named export function'),
};
});
test('should do a partial mock', () => {
// The mocked parts:
const namedExportFunctionResult = namedExportFunction();
expect(namedExportFunction).toHaveBeenCalled();
expect(namedExportFunctionResult).toBe('mocked named export function');
// The unmocked parts:
expect(namedExportVariable).toBe('named export variable');
});
Example 2: a default export
If you need to either mock a default export object, or import a default export object, you'll need to use the special keyword of __esModule: true,
.
For example, here's the test file when mocking the export default
object:
import defaultExportFunction, { namedExportVariable } from './foo-bar-baz';
jest.mock('./foo-bar-baz', () => {
const originalModule = jest.requireActual('./foo-bar-baz');
return {
__esModule: true,
...originalModule,
default: jest.fn(() => 'mocked export default function'),
};
});
test('should do a partial mock', () => {
// The mocked parts:
const defaultExportFunctionResult = defaultExportFunction();
expect(defaultExportFunction).toHaveBeenCalled();
expect(defaultExportFunctionResult).toBe('mocked export default function');
// The unmocked parts:
expect(namedExportVariable).toBe('named export variable');
});
And here's the test file when only using the export default
object, and mocking another named part of the module:
import defaultExportFunction, { namedExportVariable } from './foo-bar-baz';
jest.mock('./foo-bar-baz', () => {
const originalModule = jest.requireActual('./foo-bar-baz');
return {
__esModule: true,
...originalModule,
namedExportVariable: 'mocked named export variable',
};
});
test('should do a partial mock', () => {
// The mocked parts:
expect(namedExportVariable).toBe('mocked named export variable');
// The unmocked parts:
const defaultExportFunctionResult = defaultExportFunction();
expect(defaultExportFunctionResult).toBe('export default function');
});
In both cases, omitting the __esModule: true
would cause the tests to fail.
Scenario 4: Mock Return Values Multiple Times
A. Goal Explanation
You wanna test function A. Function A uses another function B. You need to mock the returned value with different values at each time.
B. Example code to test
None. This is just about the test itself.
C. Case Description
We can control the implementation of the mock function depending of the amount of calls made to it so far (index-based).
D. How to test
test('should do a partial mock', () => {
const myMockFn = jest
.fn(() => 1)
.mockImplementation(() => 3);
.mockImplementationOnce(() => 10)
.mockImplementationOnce(() => 42)
console.log(myMockFn()); // 10
console.log(myMockFn()); // 42
console.log(myMockFn()); // 3
console.log(myMockFn()); // 3
console.log(myMockFn()); // 3
expect(1).toBe(1);
});
When you need to recreate a complex behavior of a mock function such that multiple function calls produce different results, use the mockImplementationOnce
method. When the mocked function runs out of implementations defined with mockImplementationOnce
, it will execute the default implementation set with:
mockImplementation
- first priority! (if defined)jest.fn
- second priority (if defined)
The output of the code above would be:
10
42
3
3
3
Notice how 1
is never printed.
It's worth noting that the order of appearance of mockImplementationOnce
matters, but when mockImplementation
appears doesn't matter.
For example:
// This:
const myMockFn = jest
.fn()
.mockImplementation(() => 3);
.mockImplementationOnce(() => 10)
.mockImplementationOnce(() => 42)
// is exactly the same as this:
const myMockFn = jest
.fn()
.mockImplementationOnce(() => 10)
.mockImplementationOnce(() => 42)
.mockImplementation(() => 3);
// and also the same as this:
const myMockFn = jest
.fn(() => 3)
.mockImplementationOnce(() => 10)
.mockImplementationOnce(() => 42);
Scenario 5: Mock a return this
method
A. Goal Explanation
You wanna test Class A. Class A has method B which returns this
.
B. Example code to test
export class MyClass {
private name: string = '';
private age: number = 0;
setName(name?: string): this {
if (typeof name === 'string') this.name = name;
return this;
}
setAge(age?: number) {
if (typeof age === 'number') this.age = age;
return this;
}
logCreated() {
fetch('http://localhost:8000').then((response) => {
console.log('data is:', response);
});
return this;
}
toJSON(): { name: string; age: number } {
return { name: this.name, age: this.age };
}
}
type InitProps = {
name?: string;
age?: number;
};
export function init(props?: InitProps) {
const { name, age } = props ?? {};
const instance = new MyClass().setName(name).setAge(age).logCreated();
return instance;
}
C. Case Description
Above we have a function called init
that we want to test. init
creates an instance of MyClass
, and calls a few of its methods. The problem is that one of the methods (logCreated
in this case) is making an API request, which we want to avoid.
Our goal is:
- to write a test that tests the
init
function. - avoid the request call (do not send an API request).
D. How to test
We start off by mocking the module of MyClass
using jest.mock
.
We only want to mock MyClass.prototype.logCreated
, and for the rest of the stuff from the module to remain as the actual implementation, so we'll use jest.requireActual()
. Then, we'll mock just the logCreated
method, and since we need it to return a pointer to this
, we'll use jest's mockReturnThis
. We will import MyClass
, which will be the mocked version (since we used jest.mock
), and we'll ask questions about MyClass.prototype.logCreated
.
import { MyClass, init } from './MyClass';
jest.mock('./MyClass', () => {
const actualModule = jest.requireActual('./MyClass');
actualModule.MyClass.prototype.logCreated = jest.fn().mockReturnThis();
return actualModule;
});
describe('init function', () => {
it('should create an instance of MyClass and use real methods except logCreated', async () => {
const instance = init({ name: 'Alice', age: 30 });
// Ensure that the object is an instance of MyClass
expect(instance).toBeInstanceOf(MyClass);
// Check that the real setName and setAge methods worked
expect(instance.toJSON()).toEqual({ name: 'Alice', age: 30 });
// Ensure that logCreated was called
expect(MyClass.prototype.logCreated).toHaveBeenCalledTimes(1);
});
});
6. Prefer using spyOn
over mock
When possible, always prefer using spyOn
instead of mock
.
Scenario 1: callback function passed as argument
The example code:
export function forEach(items, callback) {
for (const item of items) {
callback(item);
}
}
The test we wrote:
import { forEach } from './forEach';
const mockCallback = jest.fn();
test('forEach mock function', () => {
const items = [0, 1];
forEach(items, mockCallback);
expect(mockCallback).toHaveBeenCalled();
expect(mockCallback).toHaveBeenCalledTimes(2);
expect(mockCallback).toHaveBeenNthCalledWith(1, 0);
expect(mockCallback).toHaveBeenNthCalledWith(2, 1);
});
The conclusion:
We had used jest.fn()
to create a function that does not exist (and named it mockCallback
). We use spyOn
on functions that do exist, and because mockCallback
did not exists, it's not right to use spyOn
in this case.
Scenario 2: mocking axios.get
The example code:
import axios from 'axios';
type Filters = {
nameStartsWith: string;
ageGreaterThan: number;
};
export class Users {
static async findMany(filters?: Filters) {
const updatedFilters: Filters = {
ageGreaterThan: filters?.ageGreaterThan ?? 0,
nameStartsWith: filters?.nameStartsWith ?? 't',
};
const response = await axios.get('/users.json', {
headers: {
'x-filter-name': updatedFilters.nameStartsWith,
'x-filter-age': updatedFilters.ageGreaterThan,
},
});
const { data } = response;
return data;
}
}
The test we wrote:
import axios from 'axios';
import { Users } from './users';
jest.mock('axios');
test('should fetch users', async () => {
(axios.get as jest.Mock).mockResolvedValue({ data: { age: 5 } });
// Would also work: (axios.get as jest.Mock).mockReturnValue({ data: { age: 5 } });
const data = await Users.findMany();
expect(axios.get).toHaveBeenCalled();
expect(axios.get).toHaveBeenCalledWith('/users.json', {
headers: { 'x-filter-name': 't', 'x-filter-age': 0 },
});
expect(data.age).toBe(5);
});
The conclusion:
This is a perfect example of when to prefer using jest.spyOn
.
We see above tht we're mocking the entire axios
module, when in fact all we want to do is mock the get
method.
A better approach is:
import axios from 'axios';
import { Users } from './users';
jest.mock('axios');
test('should fetch users', async () => {
(axios.get as jest.Mock).mockResolvedValue({ data: { age: 5 } });
const axiosGetSpy = jest.spyOn(axios, 'get').mockResolvedValue({ data: { age: 5 } });
const data = await Users.findMany();
expect(axiosGetSpy).toHaveBeenCalled();
expect(axiosGetSpy).toHaveBeenCalledWith('/users.json', {
headers: { 'x-filter-name': 't', 'x-filter-age': 0 },
});
expect(data.age).toBe(5);
});
While the difference may seem mild, it is quite large:
- We no longer need the type coercion.
- We no longer mock the entire module.
- The
axios
instance is different (mocked v.s. real). - We no longer affect non-related parts (for example,
axios.post
).
Scenario 3: mock only 1 function/constant from a file.
The example code:
export const namedExportVariable = 'named export variable';
export function namedExportFunction() {
return 'named export function';
};
export default () => 'export default function';
The test we wrote:
import { namedExportFunction, namedExportVariable } from './foo-bar-baz';
jest.mock('./foo-bar-baz', () => {
const originalModule = jest.requireActual('./foo-bar-baz');
return {
...originalModule,
namedExportFunction: jest.fn(() => 'mocked named export function'),
};
});
test('should do a partial mock', () => {
// The mocked parts:
const namedExportFunctionResult = namedExportFunction();
expect(namedExportFunction).toHaveBeenCalled();
expect(namedExportFunctionResult).toBe('mocked named export function');
// The unmocked parts:
expect(namedExportVariable).toBe('named export variable');
});
The conclusion:
Another great example of using spyOn
.
All this boilerplate of jest.mock
could be spared, by just using spyOn
:
import { namedExportFunction, namedExportVariable } from './foo-bar-baz';
test('should do a partial mock', () => {
const namedExportFunctionSpy = jest
.spyOn(require('./foo-bar-baz'), 'namedExportFunction')
.mockReturnValue('mocked named export function');
const namedExportFunctionResult = namedExportFunction();
expect(namedExportFunctionSpy).toHaveBeenCalled();
expect(namedExportFunctionResult).toBe('mocked named export function');
expect(namedExportVariable).toBe('named export variable');
});
See how much cleaner this is? ✅
We didn't need to mock the entire module, and just mocked the parts we needed.
Also, spy
functions are similar to jest.fn
functions in the sense that their implementation can be mocked, so it's not just spying.
Scenario 4: Multiple return values using mockImplementationOnce
Scenario 4 talked about mockImplementationOnce
, so there's nothing here to actually compare with spyOn
.
const myMockFn = jest
.fn()
.mockImplementation(() => 3);
.mockImplementationOnce(() => 10)
.mockImplementationOnce(() => 42)
We will say though that jest.spyOn
also supports mockImplementationOnce
, since it is similar to jest.fn
Scenario 5: mock 1 method on 1 exported object from a file
The example code:
export class MyClass {
private name: string = '';
private age: number = 0;
setName(name?: string): this {
if (typeof name === 'string') this.name = name;
return this;
}
setAge(age?: number) {
if (typeof age === 'number') this.age = age;
return this;
}
logCreated() {
fetch('http://localhost:8000').then((response) => {
console.log('data is:', response);
});
return this;
}
toJSON(): { name: string; age: number } {
return { name: this.name, age: this.age };
}
}
type InitProps = { name?: string; age?: number; };
export function init(props?: InitProps) {
const { name, age } = props ?? {};
const instance = new MyClass().setName(name).setAge(age).logCreated();
return instance;
}
The test we wrote:
import { MyClass, init } from './MyClass';
jest.mock('./MyClass', () => {
const actualModule = jest.requireActual('./MyClass');
actualModule.MyClass.prototype.logCreated = jest.fn().mockReturnThis();
return actualModule;
});
describe('init function', () => {
it('should create an instance of MyClass and use real methods except logCreated', async () => {
const instance = init({ name: 'Alice', age: 30 });
expect(instance).toBeInstanceOf(MyClass);
expect(instance.toJSON()).toEqual({ name: 'Alice', age: 30 });
expect(MyClass.prototype.logCreated).toHaveBeenCalledTimes(1);
});
});
The conclusion:
Another great example of when to prefer spyOn
.
Look at the following test:
import { MyClass, init } from './MyClass';
describe('init function', () => {
it('should create an instance of MyClass and use real methods except logCreated', async () => {
const logCreatedSpy = jest.spyOn(MyClass.prototype, 'logCreated').mockReturnThis();
const instance = init({ name: 'Alice', age: 30 });
expect(instance).toBeInstanceOf(MyClass);
expect(instance.toJSON()).toEqual({ name: 'Alice', age: 30 });
expect(logCreatedSpy).toHaveBeenCalledTimes(1);
});
});
We removed all the boilerplate of jest.mock
, and only added the spy on what we wanted, which is the logCreated
method.
Scenario 6: Just pure spying
This is a new scenario only possible with spyOn
.
What if we just want to spy on a function? We don't want to affect the function's implementation. We simply want to run assertions on it, such as: toHaveBeenCalled
or toHaveBeenCalledWith
. For that purpose, spyOn
is our ONLY option.
export function makeFood(){
return 'dinner'
}
import { makeFood } from './makeFood';
test('makeFood', () => {
const makeFoodModule = require('./makeFood');
const makeFoodSpy = jest.spyOn(makeFoodModule, 'makeFood');
const food = makeFood();
expect(food).toBe('dinner');
expect(makeFoodSpy).toHaveBeenCalled();
});
Summary
jest.mock()
- Automatically mocks an entire module.
- Used to replace all exports of a module with mock functions.
- If no implementation is provided, all the mocked functions return undefined by default.
jest.spyOn()
- Spies on an existing method of an object.
- A required module can be considered as an object.
- Allows you to track calls while still calling the original implementation, unless overridden.
- Less code is required in order to spy 1 function inside a module with multiple exports.
- THE
global
object can also be considered as an object. i.e.:jest.spyOn(global, 'setTimeout');
. But don't mocksetTimeout
, since you havejest.useFakeTimers()
already built-in.
7. Timer Mocks
A. Introduction
The native timer functions, setTimeout()
, setInterval()
, clearTimeout()
, and clearInterval()
have a NEGATIVE effect on a testing since they rely on real time to elapse, making tests slow and unreliable.
As a solution, jest has 2 functions: jest.useFakeTimers()
& jest.useRealTimers()
. The useFakeTimers
is replacing the original implementation of setTimeout()
and other timer functions, and useRealTimers
restores them to their normal behavior.
jest also has the very useful functions:
jest.runAllTimers()
jest.runOnlyPendingTimers()
B. Example code to test
export function timerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
console.log("Time's up -- stop!");
callback && callback();
}, 1000);
}
C. How to test
jest.useFakeTimers();
test('calls the callback after 1 second', () => {
const timerGame = require('../timerGame');
const callback = jest.fn();
timerGame(callback);
// At this point in time, the callback should not have been called yet
expect(callback).not.toHaveBeenCalled();
// Fast-forward until all timers have been executed
jest.runAllTimers();
// Now our callback should have been called!
expect(callback).toHaveBeenCalled();
expect(callback).toHaveBeenCalledTimes(1);
});
D. Run Pending Timers
There are also scenarios where you might have a recursive timer – that is a timer that sets a new timer in its own callback. For these, running all the timers would be an endless loop, throwing the following error: "Aborting after running 100000 timers, assuming an infinite loop!"
If that is your case, using jest.runOnlyPendingTimers() will solve the problem.
export function infiniteTimerGame(callback) {
console.log('Ready....go!');
setTimeout(() => {
console.log("Time's up! 10 seconds before the next game starts...");
callback && callback();
// Schedule the next game in 10 seconds
setTimeout(() => {
infiniteTimerGame(callback);
}, 10000);
}, 1000);
}
For debugging or any other reason you can change the limit of timers (which is by default 100000 as we said) that will be run before throwing an error:
jest.useFakeTimers({timerLimit: 100});