138

What's the best way to correctly mock the following example?

The problem is that after import time, foo keeps the reference to the original unmocked bar.

module.js:

export function bar () {
    return 'bar';
}

export function foo () {
    return `I am foo. bar is ${bar()}`;
}

module.test.js:

import * as module from '../src/module';

describe('module', () => {
    let barSpy;

    beforeEach(() => {
        barSpy = jest.spyOn(
            module,
            'bar'
        ).mockImplementation(jest.fn());
    });


    afterEach(() => {
        barSpy.mockRestore();
    });

    it('foo', () => {
        console.log(jest.isMockFunction(module.bar)); // outputs true

        module.bar.mockReturnValue('fake bar');

        console.log(module.bar()); // outputs 'fake bar';

        expect(module.foo()).toEqual('I am foo. bar is fake bar');
        /**
         * does not work! we get the following:
         *
         *  Expected value to equal:
         *    "I am foo. bar is fake bar"
         *  Received:
         *    "I am foo. bar is bar"
         */
    });
});

I could change:

export function foo () {
    return `I am foo. bar is ${bar()}`;
}

to:

export function foo () {
    return `I am foo. bar is ${exports.bar()}`;
}

but this is pretty ugly in my opinion to do everywhere.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Mark
  • 12,359
  • 5
  • 21
  • 37
  • 9
    See this issue thread on `jest` GH page https://github.com/facebook/jest/issues/936#issuecomment-545080082 – Nickofthyme Oct 23 '19 at 15:54
  • 3
    In 2021, Jest has an official way to do this "partial mocking", that doesn't require modification of the `module.js` code and is straightforward and simple/declarative to write: https://jestjs.io/docs/mock-functions#mocking-partials . – user3773048 Oct 15 '21 at 22:21
  • 2
    @user3773048 unfortunately, the 'partial mocking' can't work as expected. I tried, but it still failed to work. – Orionpax Oct 31 '22 at 10:10
  • @user3773048 Partial mocking still won't work when the function is called from the module itself – Ricola May 18 '23 at 11:17

10 Answers10

58

An alternative solution can be importing the module into its own code file and using the imported instance of all of the exported entities. Like this:

import * as thisModule from './module';

export function bar () {
    return 'bar';
}

export function foo () {
    return `I am foo. bar is ${thisModule.bar()}`;
}

Now mocking bar is really easy, because foo is also using the exported instance of bar:

import * as module from '../src/module';

describe('module', () => {
    it('foo', () => {
        spyOn(module, 'bar').and.returnValue('fake bar');
        expect(module.foo()).toEqual('I am foo. bar is fake bar');
    });
});

Importing the module into its own code looks strange, but due to the ES6's support for cyclic imports, it works really smoothly.

MostafaR
  • 3,547
  • 1
  • 17
  • 24
49

The problem seems to be related to how you expect the scope of bar to be resolved.

On one hand, in module.js you export two functions (instead of an object holding these two functions). Because of the way modules are exported the reference to the container of the exported things is exports like you mentioned it.

On the other hand, you handle your export (that you aliased module) like an object holding these functions and trying to replace one of its function (the function bar).

If you look closely at your foo implementation you are actually holding a fixed reference to the bar function.

When you think you replaced the bar function with a new one you just actually replaced the reference copy in the scope of your module.test.js

To make foo actually use another version of bar you have two possibilities :

  1. In module.js export a class or an instance, holding both the foo and bar method:

    Module.js:

    export class MyModule {
      function bar () {
        return 'bar';
      }
    
      function foo () {
        return `I am foo. bar is ${this.bar()}`;
      }
    }
    

    Note the use of this keyword in the foo method.

    Module.test.js:

    import { MyModule } from '../src/module'
    
    describe('MyModule', () => {
      //System under test :
      const sut:MyModule = new MyModule();
    
      let barSpy;
    
      beforeEach(() => {
          barSpy = jest.spyOn(
              sut,
              'bar'
          ).mockImplementation(jest.fn());
      });
    
    
      afterEach(() => {
          barSpy.mockRestore();
      });
    
      it('foo', () => {
          sut.bar.mockReturnValue('fake bar');
          expect(sut.foo()).toEqual('I am foo. bar is fake bar');
      });
    });
    
  2. Like you said, rewrite the global reference in the global exports container. This is not a recommended way to go as you will possibly introduce weird behaviors in other tests if you don't properly reset the exports to its initial state.

John-Philip
  • 3,392
  • 2
  • 23
  • 52
  • Switching to a class, while it works, still leads to the conclusion that there's no way to fully unit test es6 module functions that reference each other doesn't it? – Cody Pace Jun 02 '23 at 11:50
  • the only way i've been able to do it in the past, is to put the functions which depend on each other, in different .js / .ts files. i.e FuncA calls FuncB, if FuncA is the function undert test, you would spy on funcB and mock the the return value, however could only get it to work if funcB was in a different file. – Gweaths Aug 02 '23 at 21:23
11

fwiw, the solution I settled on was to use dependency injection, by setting a default argument.

So I would change

export function bar () {
    return 'bar';
}

export function foo () {
    return `I am foo. bar is ${bar()}`;
}

to

export function bar () {
    return 'bar';
}

export function foo (_bar = bar) {
    return `I am foo. bar is ${_bar()}`;
}

This is not a breaking change to the API of my component, and I can easily override bar in my test by doing the following

import { foo, bar } from '../src/module';

describe('module', () => {
    it('foo', () => {
        const dummyBar = jest.fn().mockReturnValue('fake bar');
        expect(foo(dummyBar)).toEqual('I am foo. bar is fake bar');
    });
});

This has the benefit of leading to slightly nicer test code too :)

Mark
  • 12,359
  • 5
  • 21
  • 37
  • 9
    I'm generally not a fan of dependency injection, since you are allowing tests to change how the code is written. That being said, this is better than the current higher-voted answer which is pretty ugly – Sean Feb 26 '18 at 00:09
  • 17
    nicer test but bad code. Not really a good idea to change your code because you cannot find a way to test it. As a developer when I look at that code, it makes me think 100x times as to why a particular method present in the module passed as a dependency to another method in same module. – Gaurav Kumar Sep 25 '18 at 09:40
10

I had this same problem and due to the project's linting standards, defining a class or rewriting references in the exports were not code review approvable options even if not prevented by the linting definitions. What I stumbled on as a viable option is to use the babel-rewire-plugin which is much cleaner, at least in appearance. While I found this used in another project I had access to, I noticed it was already in an answer in a similar question which I have linked here. This is a snippet adjusted for this question (and without using spies) provided from the linked answer for reference (I also added semicolons in addition to removing spies because I'm not a heathen):

import __RewireAPI__, * as module from '../module';

describe('foo', () => {
  it('calls bar', () => {
    const barMock = jest.fn();
    __RewireAPI__.__Rewire__('bar', barMock);
    
    module.foo();

    expect(bar).toHaveBeenCalledTimes(1);
  });
});

https://stackoverflow.com/a/45645229/6867420

Brandon Hunter
  • 141
  • 1
  • 11
9

Works for me:

cat moduleWithFunc.ts

export function funcA() {
 return export.funcB();
}
export function funcB() {
 return false;
}

cat moduleWithFunc.test.ts

import * as module from './moduleWithFunc';

describe('testFunc', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  afterEach(() => {
    module.funcB.mockRestore();
  });

  it.only('testCase', () => {
    // arrange
    jest.spyOn(module, 'funcB').mockImplementationOnce(jest.fn().mockReturnValue(true));

    // act
    const result = module.funcA();

    // assert
    expect(result).toEqual(true);
    expect(module.funcB).toHaveBeenCalledTimes(1);
  });
});
6

From this thread:

Try using a function expression

export const bar = () => {
  return "bar"
}

This should let you spy on bar even if its used by another function in the same module.

Joe Valentine
  • 69
  • 1
  • 1
1

If you define your exports you can then reference your functions as part of the exports object. Then you can overwrite the functions in your mocks individually. This is due to how the import works as a reference, not a copy.

module.js:

exports.bar () => {
    return 'bar';
}

exports.foo () => {
    return `I am foo. bar is ${exports.bar()}`;
}

module.test.js:

describe('MyModule', () => {

  it('foo', () => {
    let module = require('./module')
    module.bar = jest.fn(()=>{return 'fake bar'})

    expect(module.foo()).toEqual('I am foo. bar is fake bar');
  });

})
Sean
  • 2,412
  • 3
  • 25
  • 31
0

If you're using Babel (i.e. @babel/parser) to handle transpiling your code, the babel-plugin-explicit-exports-references1 npm package solves this pretty elegantly by making the "ugly" module.exports replacements for you transparently at transpile time. See the original problem thread for more information.


1 Note: I wrote this plugin!

Xunnamius
  • 478
  • 1
  • 8
  • 16
0

For CommonJS modules users, suppose the file looks something like:

/* myModule.js */
function bar() {
  return "bar";
}

function foo() {
  return `I am foo. bar is ${bar()}`;
}

module.exports = { bar, foo };

You need to modify the file to:

/* myModule.js */
function bar() {
  return "bar";
}

function foo() {
  return `I am foo. bar is ${myModule.bar()}`;  // Change `bar()` to `myModule.bar()`
}

const myModule = { bar, foo };  // Items you wish to export

module.exports = myModule;  // Export the object

Your original test suite (myModule.test.js) should now pass:

const myModule = require("./myModule");

describe("myModule", () => {
  test("foo", () => {
    jest.spyOn(myModule, "bar").mockReturnValueOnce("bar-mock");

    const result = myModule.foo();
    expect(result).toBe("I am foo. bar is bar-mock");
  });
});

Read more: Mock/Spy exported functions within a single module in Jest

AnsonH
  • 2,460
  • 2
  • 15
  • 29
-1

There are various hacks available here to make this work, but the real answer most people should be using is: don't. Taking the OP's example module:

export function bar () {
    return 'bar';
}

export function foo () {
    return `I am foo. bar is ${bar()}`;
}

and testing the actual behaviour, you'd write:

import { bar, foo } from "path/to/module";

describe("module", () => {
    it("foo returns 'bar'", () => {
        expect(bar()).toBe('bar');
    });

    it("foo returns 'I am foo. bar is bar'", () => {
        expect(foo()).toBe('I am foo. bar is bar');
    });
});

Why? Because then you can refactor inside the module boundary without changing the tests, which gives you the confidence to improve the quality of your code in the knowledge that it still does what it's supposed to.

Imagine you extracted the creation of 'bar' from bar to an unexported function, for example:

function rawBar() {
    return 'bar';
}

export function bar () {
    return rawBar();
}

export function foo () {
    return `I am foo. bar is ${rawBar()}`;
}

The test I suggest above would pass. If you'd asserted that calling foo meant bar got called, that test would start failing, even though the refactor preserved the module's behaviour (same API, same outputs). That's an implementation detail.

Test doubles are for collaborators, if something really does need to be mocked here it should be extracted to a separate module (then mocking it is much easier, which tells you you're moving in the right direction). Trying to mock functions in the same module is like mocking parts of a class you're trying to test, which I illustrate similarly here: https://stackoverflow.com/a/66752334/3001761.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
  • 1
    This is all nice until your inner function is an API call that you want to mock response for. Then your proposed solution doesn't work. It has been documented as an issue (somehow closed without resolving) in Jest project https://github.com/facebook/jest/issues/6972 – codeepic Feb 15 '23 at 13:06
  • @codeepic _"if something really does need to be mocked here it should be extracted to a separate module"_ – jonrsharpe Feb 15 '23 at 13:48
  • (Also in general the idea that Jest could or should provide a test boundary inside a module like this is, as well as bad test/design practice, a misunderstanding of how the functions access one another _at the JavaScript level_. It's not something Jest controls.) – jonrsharpe Feb 15 '23 at 13:58