How to stub exported function in ES6?

JavascriptEcmascript 6

Javascript Problem Overview


I have file foo.js:

export function bar (m) {
  console.log(m);
}

And another file that uses foo.js, cap.js:

import { bar } from 'foo';

export default m => {
  // Some logic that I need to test
  bar(m);
}

I have test.js:

import cap from 'cap'

describe('cap', () => {
  it('should bar', () => {
      cap('some');
  });
});

Somehow I need override implementation of bar(m) in test. Is there any way to do this?

P.S. I use babel, webpack and mocha.

Javascript Solutions


Solution 1 - Javascript

Ouch.. I found solution, so I use sinon to stub and import * as foo from 'foo' to get object with all exported functions so I can stub them.

import sinon from 'sinon';
import cap from 'cap';
import * as foo from 'foo';

sinon.stub(foo, 'bar', m => {
    console.log('confirm', m);
});

describe('cap', () => {
  it('should bar', () => {
    cap('some');
  });
});

Solution 2 - Javascript

You can replace/rewrite/stub exports only from within the module itself. (Here's an explanation)

If you rewrite 'foo.js' like this:

var bar = function bar (m) {
  console.log(m);
};

export {bar}

export function stub($stub) {
  bar = $stub;
}

You can then override it in your test like this:

import cap from 'cap'
import {stub} from 'foo'

describe('cap', () => {
  it('should bar', () => {
      stub(() => console.log('stubbed'));
      cap('some'); // will output 'stubbed' in the console instead of 'some'
  });
});

I've created a Babel plugin that transforms all the exports automatically so that they can be stubbed: https://github.com/asapach/babel-plugin-rewire-exports

Solution 3 - Javascript

While @Mike solution would work in old versions of sinon, it has been removed since sinon 3.0.0.

Now instead of:

sinon.stub(obj, "meth", fn);

you should do:

stub(obj, 'meth').callsFake(fn)

Example of mocking google oauth api:

import google from 'googleapis';

const oauth2Stub = sinon.stub(); 

sinon.stub(google, 'oauth2').callsFake(oauth2Stub);

oauth2Stub.withArgs('v2').returns({
	tokeninfo: (accessToken, params, callback) => {
		callback(null, { email: '[email protected]' }); // callback with expected result
	}
});

Solution 4 - Javascript

You can use babel-plugin-rewire (npm install --save-dev babel-plugin-rewire)

And then in test.js use the __Rewire__ function on the imported module to replace the function in that module:

// test.js
import sinon from 'sinon'

import cap from 'cap'

describe('cap', () => {
  it('should bar', () => {
    const barStub = sinon.stub().returns(42);
    cap.__Rewire__('bar', barStub); // <-- Magic happens here
    cap('some');
    expect(barStub.calledOnce).to.be.true;
  });
});

Be sure to add rewire to your babel plugins in .babelrc:

// .babelrc
{
  "presets": [
    "es2015"
  ],
  "plugins": [],
  "env": {
    "test": {
      "plugins": [
        "rewire"
      ]
    }
  }
}

Lastly, as you can see the babel-plugin-rewire plugin is only enabled in the test environment, so you should call you test runner with the BABEL_ENV environment variable set to test (which you're probably doing already):

env BABEL_ENV=test mocha --compilers js:babel-core/register test-example.js

Note: I couldn't get babel-plugin-rewire-exports to work.

Solution 5 - Javascript

This was definitely a gotcha for me too...

I created a little util to workaround this limitation of sinon. (Available in js too).

// mockable.ts 👇

import sinon from 'sinon'

export function mockable<T extends unknown[], Ret>(fn: (...fnArgs: T) => Ret) {
  let mock: sinon.SinonStub<T, Ret> | undefined
  const wrapper = (...args: T) => {
    if (mock) return mock(...args)
    return fn(...args)
  }
  const restore = () => {
    mock = undefined
  }
  wrapper.mock = (customMock?: sinon.SinonStub<T, Ret>) => {
    mock = customMock || sinon.stub()
    return Object.assign(mock, { restore })
  }
  wrapper.restore = restore
  return wrapper
}

If you paste the above snippet into your project you can use it like so

foo.js

import { mockable } from './mockable'

// we now need to wrap the function we wish to mock
export const foo = mockable((x) => {
   console.log(x)
})

main.js

import { foo } from './foo'

export const main = () => {
  foo('asdf') // use as normal
}

test.js

import { foo } from './foo'
import { main } from './main'

// mock the function - optionally pass in your own mock
const mock = foo.mock()

// test the function
main()
console.assert(mock.calledOnceWith('asdf'), 'not called')

// restore the function
stub.restore()

The benefit of this approach is that you don't have to remember to always import the function in a certain way. import { foo } from './foo' works just as well as import * as foo from './foo'. Automatic imports will likely just work in your IDE.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMike ChaliyView Question on Stackoverflow
Solution 1 - JavascriptMike ChaliyView Answer on Stackoverflow
Solution 2 - JavascriptDreamSonicView Answer on Stackoverflow
Solution 3 - JavascriptDaniel Conde MarinView Answer on Stackoverflow
Solution 4 - JavascriptqffView Answer on Stackoverflow
Solution 5 - Javascriptdavid_adlerView Answer on Stackoverflow