I was doing some basic jest unit testing in attempt to learn it more.
I have this issue I do not know how to explain
This file has the child function add
// FileB.js
const add = (a, b) => {
return a + b;
}
module.exports = {
add,
};
This file has the parent function addTen
// FileA.js
const { add } = require('./FileB');
const addTen = num => {
return add(10, num);
}
module.exports = {
addTen,
};
this is my test file, where I am trying to either check <mockedFunction/spy>.mock.calls
or do toHaveBeenCalledWith
to see if the inner child method, add, is being passed in 10,10 when i call addTen(10);
This file is not used in any real env, its simply me trying to learn jest + unit testing more.
// randomTest.js
const { addTen } = require('../src/FileA');
const fileB = require('../src/FileB');
describe('temp', () => {
it('temp', () => {
const addSpy = jest.spyOn(fileB, 'add');
addTen(10);
console.log(addSpy.mock.calls);
expect(addSpy).toHaveBeenCalledWith(10,10)
});
});
Now for the issue, the test fails, saying add
was never called, or nothing passed in. I logged the add function within FileB
However, if I modify FileA
in this way, by importing entore module instead of destructuring, the test passes and I cna log out the functions and everything and see it works
This is what works
// FileA.js
const fileB = require('./FileB');
const addTen = num => {
return fileB.add(10, num);
}
module.exports = {
addTen,
};
Why does this slight change work? And is there a way to avoid this and keep my destrcutoring?
You can achieve this by setting up your spy before importing addTen
from FileA
const fileB = require('./FileB');
const spy = jest.spyOn(fileB, 'add');
const {addTen} = require('./FileA');
describe('temp', () => {
afterEach(() => {
// restore the spy created with spyOn
jest.restoreAllMocks();
});
it('temp', () => {
addTen(10);
expect(spy).toHaveBeenCalledWith(10,10)
});
});
Or, you can mock ./FileB
with jest.mock
const mockAdd = jest.fn()
jest.mock('./FileB', ()=> {
return {
add: mockAdd
}
});
const {addTen} = require('./FileA');
describe('temp', () => {
it('temp', () => {
addTen(10);
expect(mockAdd).toHaveBeenCalledWith(10,10)
});
});
In both cases, you want your FileA
to import a mocked function from FileB
, so you would do that before importing it.
Not that jest.mock
would be hoisted anyways, so even if you don't put it at the beginning of your file, it would be moved there on the runtime
Now back to your first question "Why deconstructing the imported object make the test fail?"
First, let's remember that JavaScript pass Objects by reference. Which means the following:
const a = {
b: 1
}
const c = a;
c.b = 9;
console.log(a); // will print { b: 9 }
Which means when FileA
imports const { add } = require("./FileB")
you are using the reference to the function add
(functions are objects in JavaScript).
This leads to having the reference of the real add
function in addTen
, and then even if you use spyOn
to override FileB.add
you won't rewrite what addTen
already has.
Now in case you use const fileB = require("./FileB")
in FileA
, you get a reference of the object that has add
, that same object that you override with jest.spyOn
.
That also explains what happens when you run the spyOn before importing addTen:
it("should pass", () => {
const addSpy = jest.spyOn(fileB, "add");
const { addTen } = require("../FileA");
addTen(10);
expect(addSpy).toHaveBeenCalledWith(10, 10);
});
FileB.add
's reference will be replaced with the new mock's reference, see how this is happening under the hood: https://github.com/facebook/jest/blob/e2196cab463c5a3e15668b1d7663058b09b7c0ed/packages/jest-mock/src/index.ts#L1256.