How to stub exported function in ES6?
Asked Answered
D

5

83

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.

Dharana answered 3/1, 2016 at 10:59 Comment(6)
where do you need to override in cap or test?Eustis
In test, actually I need this to isolate cap functionality from bar for testing purposes.Dharana
What version of babel are you using?Cimino
@DavinTryon 6.3.15, I can actually use any if that matters.Dharana
If you use babel 5, you can use this plugin: github.com/speedskater/babel-plugin-rewire. But it doesn't support babel 6 yet.Cimino
Does the export default in your cap.js file have anything to do with your question? Does it matter if it is default or not?Centipede
D
102

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');
  });
});
Dharana answered 3/1, 2016 at 12:9 Comment(7)
I don't think this will work in an actual (non-babel-transpiled) ES6 environmentFernanda
@Fernanda yep, per my current understanding export actually exports immutable object, and because right now this implemented using regular objects it might fail later on.Dharana
This doesn't work like @MikeChaliy mentioned. Any other ideas?Dipody
This is working for me in a Node typescript (non-Babel) projectCancer
Sonar flags this and recommends that only the method needed be importedMalta
This feature has been removed since sinon 3.0.0- see answer https://mcmap.net/q/241507/-how-to-stub-exported-function-in-es6Catanddog
I'm getting the error: TypeError: ES Modules cannot be stubbedVamoose
P
9

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

Poaceous answered 26/10, 2016 at 16:22 Comment(0)
T
3

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
    }
});
Teage answered 7/1, 2018 at 14:20 Comment(1)
I don't think this addresses the original question about faking exported function in sinon 3.x. Can you extend your answer to address the original question?Parmenides
D
2

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.

Differentiation answered 13/8, 2017 at 20:56 Comment(0)
S
0

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.

Sectary answered 25/7, 2021 at 20:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.