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
toBeNull
matches onlynull
toBeUndefined
matches onlyundefined
toBeDefined
is the opposite oftoBeUndefined
toBeTruthy
matches anything that anif
statement treats as truetoBeFalsy
matches anything that anif
statement 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.