103

I have one file called helper.js that consist of two functions

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

export const funcB = (key,prop) => {
   return someObj;
};

I have my helper.spec.js to test the helper.js file functions.

import {funcA,funcB} from 'helper';

describe('helper', () => {
   test('testFuncB', () => {

   }
   test('testFuncA', () => {

   }
}

The test for funcB is pretty simple i just call it and expect someObj
The problem is to test funcA, in order to test it i want to mock the response of funcB.

I want testFuncB call the actual funcB and testFuncA call mocked funcB

How can i achieve funcB to be mocked and original in my two tests?

This is not a duplicate. It is a different case: they mock inner called functions only, if I remove the testFuncB then it will be the same but I must perform test on testFuncB too.

Umair Ansari
  • 173
  • 1
  • 6
Alexander Gorelik
  • 3,985
  • 6
  • 37
  • 53

8 Answers8

148

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();
  });
});
Brian Adams
  • 43,011
  • 9
  • 113
  • 111
  • What to do when I get `(function (exports, require, module, __filename, __dirname) { import `? – unknown_boundaries Sep 10 '19 at 02:22
  • @Prakhar you'll need to [compile with something like Babel](https://jestjs.io/docs/en/getting-started#using-babel) since ES6 module support in Node.js [is still just experimental](https://nodejs.org/dist/latest-v12.x/docs/api/esm.html#esm_ecmascript_modules) – Brian Adams Sep 10 '19 at 02:58
  • @Prakhar in the above example use `exports.funcB(key)` inside the `funcA` – Jaime Sangcap Sep 19 '19 at 08:58
  • @BrianAdams how do i spy only 1 particular function that is nested; through the second approach? for example: funcA calls {funcB, funcC, funcD,} and funcC in turn calls {funcCC} which in turn calls {funcCCC}, i want to write test for funcA by mocking just funcCCC – Ganga Jul 28 '21 at 17:18
  • 1
    @Ganga `funcCCC` just needs to be exported and `funcCC` needs to call it by using the module. If `funcCC` calls `funcCCC` using the module then the module export for `funcCCC` can be mocked during the test. – Brian Adams Jul 28 '21 at 17:24
  • When I run the first example I get `TypeError: (0 , _helper.funcA) is not a function` – guero64 Jan 14 '22 at 14:48
  • You mentioned "not within a class, object". Does that mean things are different if the functions are within a class or object? – Betty Jun 30 '23 at 20:10
8

Late answer but this should work. Also you should test funcB in its own file and not inside the 'helper' tests.

import { funcB } from './funcB';
import { funcA } from './helper';

jest.mock('./funcB');

describe('helper', () => {
    test('test funcA', () => {
        const funcBSpy = jest.fn();
        funcB.mockImplementation(() => funcBSpy());
        
        funcA();

        expect(funcBSpy).toHaveBeenCalledTimes(1);
    });
});
Mattia Vio
  • 134
  • 1
  • 6
3

I create a kind of nameSpace to handle this issue:

let helper = {}

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

const funcB = (key,prop) => {
    return someObj;
};

helper = { funcA, funcB }

module.exports = helper

and then mocking is obvious with jest.fn

BB_KING
  • 145
  • 2
  • 13
2
import * as helper from 'helper';

    describe('helper', () => {
       it('should test testFuncA', () => {
          const mockTestFuncB = jest.mock();
          // spy on calls to testFuncB and respond with a mock function

           mockTestFuncB.spyOn(helper, 'testFuncB').mockReturnValue(/*your expected return value*/);

          // test logic

          // Restore helper.testFuncB to it's original function
          helper.testFuncB.mockRestore();
       }
    }
hannad rehman
  • 4,133
  • 3
  • 33
  • 55
2

You can use babel-plugin-rewire provided __set__ function to mock internal function.

Assuming you have set up babel-plugin-rewire.

helper.spec.js

import {funcA, __set__} as helper from './helper';

describe('helper', () => {
  test('test funcA', () => {
    __set__('funcB', () => {
      return 'funcB return value'
    })

    expect(funcA()).toBe('funcB return value'); 
  });
});

One advantage of this solution is you don't need to change any original code

Yang Liu
  • 101
  • 1
  • 2
1

I was able to get this working. I separated my helper and my main logic into two files like other solutions. In the test file, I had to mock the entire helper file.

const { doAdd } = require('./addHelper');

function add(a, b) {
  return doAdd(a, b);
}
jest.mock('./addHelper');

// ...

it('should call doAdd', () => {
  // hook into doAdd helper method and intercept its return value
  jest.spyOn(helperModule, 'doAdd').mockReturnValue(11);

  expect(addModule.add()).toEqual(11);
  expect(helperModule.doAdd).toBeCalled();
});

Here is my solution:

https://github.com/davidholyko/jest-sandbox

David Ko
  • 304
  • 1
  • 3
  • 11
-1

I think this might work

import * as helper from 'helper';

describe('helper', () => {
   test('testFuncB', () => {

   }
   test('testFuncA', () => {
      const mockTestFuncB = jest.mock();
      // spy on calls to testFuncB and respond with a mock function
      jest.spyOn(helper, 'testFuncB').mockImplementationOnce(mockTestFuncB);

      // Do the testing ...

      // Restore helper.testFuncB to it's original function
      helper.testFuncB.mockRestore();
   }
}
Red Mercury
  • 3,971
  • 1
  • 26
  • 32
  • 5
    Note for new viewers of this question: this doesn't work. See [this answer](https://stackoverflow.com/a/55193363/10149510) for an explanation and working example. – Brian Adams Mar 16 '19 at 04:11
-1

You can do the following trick when you test the funcA:

1.Mock the funcB:

helper.funcB = jest.fn().mockImplementationOnce(() => <your data>);

2.Change the funcB(key) to this.funcB(key)

I had the same problem and worked! Full Code:

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

export const funcB = (key,prop) => {
    return someObj;
};

Test Code:

import helper from 'helper';

describe('helper', () => {
   test('testFuncB', () => {
       ...
   }
   test('testFuncA', () => {
       helper.funcB = jest.fn().mockImplementationOnce(() => <your data>);
   }
}
mspapant
  • 1,860
  • 1
  • 22
  • 31