Jest Mock Inner Function

Jest mock inner function

If an ES6 module directly exports two functions (not within a class, object, etc., just directly exports the functions like in the question) and one directly calls the other, then that call cannot be mocked.

In this case, funcB cannot be mocked within funcA the way the code is currently written.

A mock replaces the module export for funcB, but funcA doesn't call the module export for funcB, it just calls funcB directly.


Mocking funcB within funcA requires that funcA call the module export for funcB.

That can be done in one of two ways:


Move funcB to its own module

funcB.js

export const funcB = () => {
return 'original';
};

helper.js

import { funcB } from './funcB';

export const funcA = () => {
return funcB();
};

helper.spec.js

import * as funcBModule from './funcB';
import { funcA } from './helper';

describe('helper', () => {

test('test funcB', () => {
expect(funcBModule.funcB()).toBe('original'); // Success!
});

test('test funcA', () => {
const spy = jest.spyOn(funcBModule, 'funcB');
spy.mockReturnValue('mocked');

expect(funcA()).toBe('mocked'); // Success!

spy.mockRestore();
});
});

Import the module into itself

"ES6 modules support cyclic dependencies automatically" so it is perfectly valid to import a module into itself so that functions within the module can call the module export for other functions in the module:

helper.js

import * as helper from './helper';

export const funcA = () => {
return helper.funcB();
};

export const funcB = () => {
return 'original';
};

helper.spec.js

import * as helper from './helper';

describe('helper', () => {

test('test funcB', () => {
expect(helper.funcB()).toBe('original'); // Success!
});

test('test funcA', () => {
const spy = jest.spyOn(helper, 'funcB');
spy.mockReturnValue('mocked');

expect(helper.funcA()).toBe('mocked'); // Success!

spy.mockRestore();
});
});

Mocking nested functions with jest

jest.mock() will mock the Repositories module and its functions exported will be auto mocked. This means the second mock factory function is optional. Since initRepo function is already mocked and has no resolved value, you don't need to mock it again. We should focus on mocking the getOrganisationsRepo function and its resolved/rejected value.

E.g.

getOrganisations.js:

import { initRepo, getOrganisationsRepo } from './Repositories';

export const getOrganisations = async (event) => {
try {
await initRepo();
const repo = await getOrganisationsRepo();
const data = await repo.getOrganisations();

// 200: Response is an array & is not empty
if (Array.isArray(data) && data.length) return { statusCode: 200, body: JSON.stringify(data) };

// 404: Response is an array and is empty
if (Array.isArray(data) && !data.length) return { statusCode: 404 };

return { statusCode: 400 };
} catch (error) {
const errorMessage = error?.message || 'error fetching organisations';

return { statusCode: 400, body: errorMessage };
}
};

Repositories.js:

export async function initRepo() {
console.log('initRepo real implementation');
}
export async function getOrganisationsRepo() {
console.log('getOrganisationsRepo real implementation');
}

getOrganisations.test.js:

import { getOrganisations } from './getOrganisations';
import { getOrganisationsRepo } from './Repositories';

jest.mock('./Repositories');

describe('68805379', () => {
afterAll(() => {
jest.resetAllMocks();
});
test('should return array data', async () => {
const repo = {
getOrganisations: jest.fn().mockResolvedValueOnce([{ id: 1 }]),
};
getOrganisationsRepo.mockResolvedValueOnce(repo);
const actual = await getOrganisations();
expect(actual).toEqual({ statusCode: 200, body: JSON.stringify([{ id: 1 }]) });
expect(getOrganisationsRepo).toBeCalledTimes(1);
expect(repo.getOrganisations).toBeCalledTimes(1);
});
});

test result:

 PASS  examples/68805379/getOrganisations.test.js (9.919 s)
68805379
✓ should return array data (5 ms)

---------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
---------------------|---------|----------|---------|---------|-------------------
All files | 63.16 | 21.43 | 50 | 60 |
Repositories.js | 50 | 100 | 0 | 50 | 2,5
getOrganisations.js | 66.67 | 21.43 | 100 | 63.64 | 13-19
---------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 11.002 s

P.S. I use relative path ./Repositories instead of module alias when using jest.mock(), if you have been set up module alias, then you should use module alias as the module name for jest.mock().

Mock inner function of a functional component

You can not mock inner functions, so the right thing to do here would probably be to refactor such that it is more easily testable.

For example, you could break out an inner component which consumes a custom draw rather than defines one, then the outer component would pass down your concrete current draw implementation.

Then you can test the inner component with a passed in mock.

Jest mock function inside function

The question you linked to has a currently accepted answer that doesn't work. I added a new answer with an explanation and working example.

The same concept applies here: a mock replaces the module export of a function so to be able to mock countriesList within getSortedCountryData you have to call the module export for countriesList.

One option is to move countriesList to its own module.

The other option is to take advantage of the fact that "ES6 modules support cyclic dependencies automatically" so it is perfectly valid to import the module into itself so that you can call the module export for countriesList:

countries.js

import * as countyListHelper from './countries';

export const countriesList = () => [
{
label: '+244',
value: 'Angola',
}, // list of all possible countries very long...
];

export const getSortedCountryData = intlLang =>
countyListHelper.countriesList()
.sort((compare, comparable) =>
compare.value.localeCompare(comparable.value, intlLang, { sensitivity: 'base' }));

countries.test.js

import * as countyListHelper from './countries';

describe('countries list', () => {
test('returns list of countries', () => {

const expectedList = [
{
label: '+244',
value: 'Angola',
},
{
label: '+43',
value: 'Austria',
},
];

const spy = jest.spyOn(countyListHelper, 'countriesList');
spy.mockReturnValue(expectedList);

expect(countyListHelper.getSortedCountryData('en')).toEqual(expectedList); // Success!

spy.mockRestore();
});
});

Jest mock nested function from another file

Below example use "jest": "^26.6.3".

user.js:

const { getAge } = require('./age');

async function isMinor() {
const bYear = 1991;
const age = await getAge(bYear);
console.log('age: ', age);
if (age <= 18) {
return true;
}
return false;
}

module.exports = { isMinor };

Option 1: use jest.mock() in beforeEach hook functional scope, it will NOT be hoised to the top of the code. So you need to require modules after mocking by jest.mock() method.

describe('Age Test', () => {
beforeEach(() => {
jest.mock('./age', () => ({
getAge: jest.fn(() => 99),
}));
});

test('should be an adult', async () => {
const { isMinor } = require('./user');
const { getAge } = require('./age');
const actual = await isMinor();
expect(actual).toBeFalsy();
expect(getAge).toBeCalledWith(1991);
});
});

unit test result:

 PASS  examples/66288290/user.test.js
Age Test
✓ should be an adult (1911 ms)

console.log
age: 99

at examples/66288290/user.js:6:11

----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 87.5 | 50 | 100 | 87.5 |
user.js | 87.5 | 50 | 100 | 87.5 | 8
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.197 s

Option 2: use jest.mock() in the module scope, it will be hoisted to the top of the code. Even if you require the modules at the top of the file. The ./age module you require is already be mocked.

const { isMinor } = require('./user');
const { getAge } = require('./age');

jest.mock('./age', () => ({
getAge: jest.fn(() => 99),
}));

describe('Age Test', () => {
afterAll(() => {
jest.resetAllMocks();
});

test('should be an adult', async () => {
const actual = await isMinor();
expect(actual).toBeFalsy();
expect(getAge).toBeCalledWith(1991);
});
});

unit test result:

 PASS  examples/66288290/user.test.js
Age Test
✓ should be an adult (11 ms)

console.log
age: 99

at examples/66288290/user.js:6:11

----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 87.5 | 50 | 100 | 87.5 |
user.js | 87.5 | 50 | 100 | 87.5 | 8
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.502 s


Related Topics



Leave a reply



Submit