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 onceexpect(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 onceexpect(mockFunc).toHaveBeenCalled();
// The mock function was called at least once with the specified argsexpect(mockFunc).toHaveBeenCalledWith(arg1, arg2);
// The last call to the mock function was called with the specified argsexpect(mockFunc).toHaveBeenLastCalledWith(arg1, arg2);
// All calls and the name of the mock is written as a snapshotexpect(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 undefinedconst myMock = jest.fn();console.log(myMock());
// Mock can be chainedmyMock.mockReturnValueOnce(10).mockReturnValueOnce('x').mockReturnValue(true);
console.log(myMock(), myMock(), myMock(), myMock());// > 10, 'x', true, trueMocking 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 automockingconst foo = require('../foo');
// foo is a mock functionfoo.mockImplementation(() => 42);foo();// > 42We 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:
// Define a mock implementation for the user module
const user = { getThemAll: jest.fn().mockReturnValue(['John', 'Jane', 'Doe']),};
export default user;And your test file
import Employees from './Employees'; // Import the module to be testedimport 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:
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.