Installation and Jest setup
Refer to https://jestjs.io/docs/getting-started
Matchers
Full list of matchers: https://jestjs.io/docs/expect
Matching exact equality with toBe
test('two plus two is four', () => {
expect(2 + 2).toBe(4);
});
Matching value equality with toEqual
test('object assignment', () => {
const data = {one: 1};
data['two'] = 2;
expect(data).toEqual({one: 1, two: 2});
});
Matching opposites with not
test('two plus two is four', () => {
expect(2 + 2).not.toBe(22);
});
Matching truthiness, null, undefined and defined
toBeNullmatches onlynulltoBeUndefinedmatches onlyundefinedtoBeDefinedis the opposite oftoBeUndefinedtoBeTruthymatches anything that anifstatement treats as truetoBeFalsymatches anything that anifstatement treats as false
Matching numbers
test('two plus two', () => {
const value = 2 + 2;
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(4.5);
// toBe and toEqual are equivalent for numbers
expect(value).toBe(4);
expect(value).toEqual(4);
});
Matching floating point numbers
test('adding floating point numbers', () => {
const value = 0.1 + 0.2;
//expect(value).toBe(0.3); This won't work because of rounding error
expect(value).toBeCloseTo(0.3); // This works.
});
Matching strings using regular expression with toMatch
test('there is no I in team', () => {
expect('team').not.toMatch(/I/);
});
test('but there is a "stop" in Christoph', () => {
expect('Christoph').toMatch(/stop/);
});
Matching arrays and iterables with toContain
const shoppingList = [
'diapers',
'kleenex',
'trash bags',
'paper towels',
'milk',
];
test('the shopping list has milk on it', () => {
expect(shoppingList).toContain('milk');
expect(new Set(shoppingList)).toContain('milk');
});
Matching exceptions with 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
});
Testing Promises
test('the data is peanut butter', () => {
return fetchData().then(data => {
expect(data).toBe('peanut butter');
});
});
Make sure to return the Promise, otherwise the test will complete prematurely.
Testing async/await with .resolves and .rejects
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');
});
Testing callbacks with done
test('the data is peanut butter', done => {
function callback(error, data) {
if (error) {
done(error);
return;
}
try {
expect(data).toBe('peanut butter');
done();
} catch (error) {
done(error);
}
}
fetchData(callback);
});
Inspecting a jest.fn() mock
jest.fn() is used to create a new, standalone mock function. It’s not tied to any specific object or method, and you can use it to replace any function in your test code. The mock property on a jest.fn() tracks calls, results, contexts, instances and lastCall:
const myMock1 = jest.fn();
const a = new myMock1();
console.log(myMock1.mock.instances);
// > [ <a> ]
const myMock2 = jest.fn();
const b = {};
const bound = myMock2.bind(b);
bound();
console.log(myMock2.mock.contexts);
// The function was called exactly once
expect(someMockFunction.mock.calls).toHaveLength(1);
// The first arg of the first call to the function was 'first arg'
expect(someMockFunction.mock.calls[0][0]).toBe('first arg');
// The first argument of the last call to the function was 'test'
expect(someMockFunction.mock.lastCall[0]).toBe('test');
Alternatively, there are convenient matchers:
// The mock function was called at least once expect(mockFunc).toHaveBeenCalled(); // The mock function was called at least once with the specified args expect(mockFunc).toHaveBeenCalledWith(arg1, arg2); // The last call to the mock function was called with the specified args expect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2); // All calls and the name of the mock is written as a snapshot expect(mockFunc).toMatchSnapshot();
Spying on implementation with jest.spyOn
jest.spyOn is primarily used to spy on existing functions or methods, tracking their calls and behavior without modifying their implementation.
const obj = {
method: () => {}
};
const spy = jest.spyOn(obj, 'method');
// Now 'method' is a spy, you can check if it's been called, etc.
obj.method();
expect(spy).toHaveBeenCalled();
Mocking jest.fn() return values
// By default a mock returns undefined
const myMock = jest.fn();
console.log(myMock());
// Mock can be chained
myMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock());
// > 10, 'x', true, true
Mocking implementation of a function with jest.fn or the mockImplementation
Instead of just mocking the return values, we can mock the whole functionality of a function:
jest.mock('../foo'); // this happens automatically with automocking
const foo = require('../foo');
// foo is a mock function
foo.mockImplementation(() => 42);
foo();
// > 42
We can also change implementation on a per-invocation basis:
const myMockFn = jest .fn(() => 'default') .mockImplementationOnce(() => 'first call') .mockImplementationOnce(() => 'second call'); console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn()); // > 'first call', 'second call', 'default', 'default'
Mocking functions that must return this keyword (to allow chaining):
const myObj = {
myMethod: jest.fn().mockReturnThis(),
};
// is the same as
const otherObj = {
myMethod: jest.fn(function () {
return this;
}),
};
Adding a name with mockName
To be able to quickly identify the mock function reporting an error in your test output.
const myMockFn = jest
.fn()
.mockReturnValue('default')
.mockImplementation(scalar => 42 + scalar)
.mockName('add42');
Mocking a whole module with jest.mock
This code uses a third-party module axios to make a request.
import axios from 'axios';
class Users {
static all() {
return axios.get('/users.json').then(resp => resp.data);
}
}
export default Users;
We want to mock the whole axios module, especially axios.get function to prevent making a real request. By specifying jest.mock('axios') jest automatically mocks the whole module, effectively replacing every method in axios module with a jest.fn() function, giving us the possibility to return custom mock-values.
import axios from 'axios';
import Users from './users';
jest.mock('axios');
test('should fetch users', () => {
const users = [{name: 'Bob'}];
const resp = {data: users};
axios.get.mockResolvedValue(resp);
// or you could use the following depending on your use case:
// axios.get.mockImplementation(() => Promise.resolve(resp))
return Users.all().then(data => expect(data).toEqual(users));
});
Mocking a whole module with jest.mock and __mocks__ folder (Manual mocks)
https://jestjs.io/docs/manual-mocks
Let’s assume your code looks like that:
import user from './user';
class Employees {
static all() {
return user.getThemAll();
}
}
export default Employees;
You want to mock the whole user module. First locate user.js, then create a folder __mocks__ on the same level, then add an empty user.js to __mocks__ folder and insert your mock code:
// __mocks__/user.js
// Define a mock implementation for the user module
const user = {
getThemAll: jest.fn().mockReturnValue(['John', 'Jane', 'Doe']),
};
export default user;
And your test file
// employees.test.js
import Employees from './Employees'; // Import the module to be tested
import user from './user'; // Import the mocked user module
describe('Employees', () => {
// Test suite for the Employees class
// Test case for the static all() method
describe('all', () => {
it('returns an array of employees', () => {
// Define a mock implementation for user.getThemAll()
user.getThemAll.mockReturnValue(['John', 'Jane', 'Doe']);
// Call the static all() method of Employees
const employees = Employees.all();
// Assertions
expect(employees).toEqual(['John', 'Jane', 'Doe']); // Check if the return value is as expected
expect(user.getThemAll).toHaveBeenCalled(); // Check if user.getThemAll() has been called
});
});
});
Mocking parts of a module with jest.requireActual
Let’s say we have this module
export const foo = 'foo'; export const bar = () => 'bar'; export default () => 'baz';
We only want to mock it partially, for example we only want to mock the default export and foo but leave bar untouched:
//test.js
import defaultExport, {bar, foo} from '../foo-bar-baz';
jest.mock('../foo-bar-baz', () => {
const originalModule = jest.requireActual('../foo-bar-baz');
//Mock the default export and named export 'foo'
return {
__esModule: true,
...originalModule,
default: jest.fn(() => 'mocked baz'),
foo: 'mocked foo',
};
});
test('should do a partial mock', () => {
const defaultExportResult = defaultExport();
expect(defaultExportResult).toBe('mocked baz');
expect(defaultExport).toHaveBeenCalled();
expect(foo).toBe('mocked foo');
expect(bar()).toBe('bar');
});
Partial mocking makes sense when you test code that still relies partly on the real implementation of the module. Consider you have this code:
import fetch from 'node-fetch';
export const createUser = async () => {
const response = await fetch('https://website.com/users', {method: 'POST'});
const userId = await response.text();
return userId;
};
You write a test an completely mock node-fetch:
jest.mock('node-fetch');
import fetch, {Response} from 'node-fetch';
import {createUser} from './createUser';
test('createUser calls fetch with the right args and returns the user id', async () => {
fetch.mockReturnValue(Promise.resolve(new Response('4')));
const userId = await createUser();
expect(fetch).toHaveBeenCalledTimes(1);
expect(fetch).toHaveBeenCalledWith('https://website.com/users', {
method: 'POST',
});
expect(userId).toBe('4');
});
Running the test creates an error TypeError: response.text is not a function. That is because you unintentionally mocked a part of node-fetch that is responsible for properly handling a Response. To solve this, you need to tell Jest to exclude the Response object from mocking. You can do this by adding requireActual:
jest.mock('node-fetch');
import fetch from 'node-fetch';
const {Response} = jest.requireActual('node-fetch');
Automatic mocks vs Manual mocks
Automatic Mocks with jest.mock()
Advantages:
- Ease of Use: It’s convenient to use
jest.mock()directly in your test file. You simply specify the module you want to mock, and Jest takes care of replacing all its exports with mock functions. - Granular Control:
jest.mock()allows you to mock specific functions or classes within a module, providing granular control over what gets mocked and what doesn’t. - Dynamic Mocking: You can dynamically adjust the behavior of mocked functions using
mockReturnValue(),mockResolvedValue(), etc., based on your test scenarios. - Local Scope: Mocks applied with
jest.mock()are local to the test file where they’re defined, making it easier to manage and reason about the mocks in each test.
Disadvantages:
- Verbose Setup: In some cases, setting up mocks using
jest.mock()can be verbose, especially when dealing with multiple mocked functions or complex mock configurations. - Less Discoverable: Since mocks are defined within the test file, it may be less discoverable for other developers who are not familiar with the test suite.
Manual Mocks with __mocks__ Folder
Advantages:
- Separation of Concerns: Manual mocks in the
__mocks__folder keep your test code cleaner and separate from your source code, promoting a clearer separation of concerns. - Global Configuration: Mocks defined in the
__mocks__folder are global, meaning they apply across all test files that import the mocked module. This can be advantageous for ensuring consistent behavior across tests. - Simplified Test Code: Test files become simpler as they don’t need to define mocks explicitly. The mocked implementations are automatically applied when importing the module.
Disadvantages:
- Less Granular Control: Manual mocks may not offer as granular control over mocking specific functions or classes within a module compared to
jest.mock(). - Potential Overhead: The
__mocks__folder might introduce overhead in larger projects, especially if multiple modules require manual mocks, potentially cluttering the project structure. - Global Scope: Since mocks defined in the
__mocks__folder are global, they can lead to unintended consequences if not used carefully, potentially affecting unrelated tests.
In summary, choosing between automatic mocks with jest.mock() and manual mocks using the __mocks__ folder depends on factors like project size, complexity, and developer preference. jest.mock() offers more granular control and local scope, while __mocks__ provides a cleaner separation of concerns and global configuration. Consider these factors when deciding which approach best suits your testing needs.