Jest — How To Mock a Function Call Inside a Module
Mocking function calls within a module
Let’s say you have the file:
// f.jsexport function b(){
return 'b';
}export function a(){
return b();
}
If you want to mock b
to test a, well… It is not as easy as it seems to be.
The Naive Approach
What I initially tried to do was:
test('a', () => {
const f = require('./f'); jest.spyOn(f, 'b').mockReturnValue('c'); expect(f.a()).toBe('c');
// FAILED!
// expected 'c' got 'b'
})
Nope… It won’t work.
That is because the exported b
is mocked indeed, but it’s not the same b
as the one that is called by a
inside the module.
Solution 1 — Splitting The Module Into Different Files
If you move b
to its own file:
// b.jsexport function b(){
return 'b';
}// f.jsimport {b} from './b';export function a(){
return b();
}
Then the test will pass:
test('a', () => {
const b = require('./b');
const f = require('./f'); jest.spyOn(b, 'b').mockReturnValue('c'); expect(f.a()).toBe('c');
//PASSED!
})
This looks like the cleanest solution, but what if you want to keep your functions in the same file?
Solution 2 — Calling The Mocked Function Using Exports
Add exports.
before calling the function
// f.jsexport function b(){
return 'b';
}export function a(){
return exports.b();
}
And then the test will just pass:
test('a', () => {
const f = require('./f'); jest.spyOn(f, 'b').mockReturnValue('c'); expect(f.a()).toBe('c');
//PASSED!
})
I really like this solution because you change only something insignificant in the tested file. Moreover, even this small change can be avoided:
You can use the library babel-plugin-explicit-exports-references
to add exports.
to all functions within the same module programmatically.
Notice that the library is very fresh and has a very small audience.
Use it with caution in terms of security.
A Sub-Optimal Solution Worth Mentioning — Exporting a Namespace Object
You can create a namespace that you export as the default object and call b
using the namespace.
This way, when you call jest.mock
it will replace the b
function on the namespace object.
// f.jsconst f = {
b(){
return 'b';
}, a(){
return f.b();
}
};export default f;
And then the test will pass:
test('a', () => {
const f = require('./f'); jest.spyOn(f, 'b').mockReturnValue('c'); expect(f.a()).toBe('c');
//PASSED!
})
But then you will need to import it as a default
import without being able to break it into named exports.
// won't work:
// import {a} from './f';import f from './f';...f.a();
Which is kinda ugly.
Another Sub-Optimal Solution— Using a Re-Wire Library
The library babel-plugin-rewire is an intrusive library that changes what’s inside modules.
It doesn’t seem to be a very maintained library and it actually didn’t work for me because it doesn’t support TypeScript, but here is roughly how it is supposed to work —
You don’t need to change anything on the file that you are testing.
// f.jsexport function b(){
return 'b';
}export function a(){
return b();
}
Rewire the b
function in the test:
import {a, __set__} from './f';test('a', () => {
const f = require('./f'); // This rewires b to return 'c'
__set__('b', () => 'c'); expect(f.a()).toBe('c');
// PASSED!})
Happy Testing :)