Search code examples
javascriptnode.jsunit-testingjestjs

Attempting to mock child method, fails if parent module imports it using object destructuring


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?


Solution

  • 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.