Sinon: Simple stub fails for ES6 import

Created on 27 Feb 2018  ·  18Comments  ·  Source: sinonjs/sinon

  • Sinon version : 4.4.2
  • Environment : MacOSX High Sierra 10.13.3
  • Example URL :
  • Other libraries you are using: Webpack, babel, mocha

What did you expect to happen?
Given a module like so:

// mod1.js
function test() {
  return 'fail';
}

export {
  test
};

And a test like this:

// mod1Spec.js
import { assert } from 'chai';
import sinon from 'sinon';

import * as mod1 from 'Modules/settings/mod1';

describe('test', function () {
  it('should correctly mock module method', () => {
    sinon.stub(mod1, 'test').returns('pass');
    assert.strictEqual(mod1.test(), 'pass');
  });
});

The test should pass.

What actually happens
The method is never mocked. It still returns 'fail' even with the direct stub above it. I've seen some issues around mocking ES6 modules, but they all seem to imply that using import * as blah will allow you to stub correctly (ES6 classes may be a different story).

Can you actually stub ES6 modules with sinon? If so, are there any known libraries that can interfere with the stubbing? I imagine babel or webpack would be valid culprits as they may bundle/transpile modules and break stubbing.

I've been tearing my code apart, so I want to confirm that it's possible to stub ES6 modules without using anything like babel-plugin-rewire or if something like that plugin is now required.

How to reproduce
See code above.

Most helpful comment

I've created a runnable test from your example, using @std/esm instead of babel.

It shows very clearly that (under node), we're not allowed to modify modules that are imported using import. It looks like imports are read-only views on exports.

So, sinon.stub is not able to modify the imported module.

I guess you will have to use some link seam with your chosen module loader to redefine what the dependencies mean.

If you're going to use a link seam, you might be interested in the new sinon.fake api (install as sinon@next). The api is still under development, feedback is very welcome.

I wonder if there's a way for us to detect that we're using modules, and thus can't replace properties on imports. If there is, at least we can provide a useful error message to users, so they won't have to spend a lot of time figuring out why things are failing in unexpected ways.

All 18 comments

I've created a runnable test from your example, using @std/esm instead of babel.

It shows very clearly that (under node), we're not allowed to modify modules that are imported using import. It looks like imports are read-only views on exports.

So, sinon.stub is not able to modify the imported module.

I guess you will have to use some link seam with your chosen module loader to redefine what the dependencies mean.

If you're going to use a link seam, you might be interested in the new sinon.fake api (install as sinon@next). The api is still under development, feedback is very welcome.

I wonder if there's a way for us to detect that we're using modules, and thus can't replace properties on imports. If there is, at least we can provide a useful error message to users, so they won't have to spend a lot of time figuring out why things are failing in unexpected ways.

This is outside the scope of what sinon is supposed to do, as it deals with a whole range of complexity that is outside of our control. If you open this codepen in Chrome and open the console, you will see it is not possible to alter a module's exports _in a fully spec compliant system_ (which Babel is not, but Chrome is), resulting in

Cannot assign to read only property 'test' of object '[object Module]'

Babel is another matter entirely, as the code produced when transpiling ES6 modules to ES5 CommonJS modules doesn't conform to the ES6 module spec (parse time vs dynamic evaluation time exports, etc), and is more or less an internal detail of Babel. That means the code above _should_ work, but I messed enough around with just setting up that simple Codepen to demonstrate ES6 loading that I won't go down that additional rabbit hole ...

See the tips mentioned in #1623 for going further (proxyquire or rewire are probably required). Closing as out of scope.

@mroderick I used 2 hours in from starting to answer this until actually submitting the reply, so didn't see your reply until after I submitted. If this is really a feature request for better error messages I think we should open a separate issue to make the reading a bit clearer.

If this is really a feature request for better error messages I think we should open a separate issue to make the reading a bit clearer.

I was looking into whether or not it is even possible to detect when running in ESM compliant mode. AFAICT, it is not.

I suppose we can make an improvement by wrapping the replacement of the property in a try-catch, and then give a nicer error message. Shall we do that?

For cases like above, where we transpile, detecting probably won't work, but for ESM shouldn't we be able to just do a type check on objects using toString and === '[object Module]'?

shouldn't we be able to just do a type check on objects using toString and === '[object Module]'?

Good idea

In my experiments (in node 8.9.4), a module import doesn't have a toString method, and using Object.prototype.toString returns [object Object].

Maybe I am missing something. Do you you have a working example of this?

I don't - I just assumed the toString() method is where the error (mentioned above) got the [object Module] bit from. I can try looking into it a little bit.

OK, looked into it, and I think you have missed something :smirk: The behaviour I saw in Chrome was reproduced in Node 6.12, 8.0, 8.7 and 9.2. I found the required info in the Ecma 262 spec for Module and this snippet on MDN.

I basically forked your repo and added some tests to verify it:

  the Module object
    ✓ should have a toStringTag symbol with the expected value
    ✓ should use the toStringTag symbol 

This should be a trivial change, so maybe I can just supply a PR to throw an error for it?

@fatso83 @mroderick Thanks a ton for the replies. I totally understand that this is outside scope of sinon so I appreciate the help in understanding the issue.

I really like the idea of a warning/error when users try to stub modules imported using import. Hopefully that will help anyone attempting to do the same thing I did.

I'll have to explore alternatives or potentially just stick with RequireJS and not "upgrade" to ES6 import/export.

Pull request in queue: #1715.

Check out the diff, @ctaylo21, to see if this is approximately what you would expect _if running in a ES Module support system_.

I don't think this would necessarily help you out in your specific case, unfortunately, as a transpiled ES Module is no longer a true ES Module ...

@ctaylo21 As I mentioned above, I was surprised to hear that the code you supplied did not work after transpiling. I just tried recreating the issue with a fresh project that uses Babel to transpile your example and it works fine, so I suspect you are not doing _exactly_ what that example code shows?

I suspect you have fallen victim to the same issues as #1248 and #1648 that basically has to do with how one in a test can stub exports in a module that is used by _another_ module. This is basically a CJS issue, so read up on that using the links below, as you can achieve it without proxyquire and similar "require middleware" (working with _link seams_). I think you'll find these links interesting for achieving what you want if you want to avoid extra dependencies:

@fatso83 I can confirm that your branch does not throw an exception when I try to stub an imported module in my code. I verified the check

if (
       object &&
       typeof Symbol !== "undefined" &&
       object[Symbol.toStringTag] === "Module"
   ) {
       throw new TypeError("No support for stubbing ES Modules");
   }

is false and I assume this is likely due to Babel transpiling the code.

I don't know why your clean setup works but mine doesn't. I'll have to dig into that.

My main issue is that I'm trying to upgrade a large codebase off a legacy build system. As a part of that refactor, I was hoping to use import/export and stop using RequireJS. Dependency injection and/or middleware are valid solutions, but would likely require a lot of rewriting of thousands of tests. It seems like our options are either rewrite a lot of our modules, use link seams, or stay with RequireJS.

Thanks again for all of your help, I really appreciate it.

If you do decide to go with link seams targeting module loaders, here are my favourites

I know that there are similar solutions for when using Webpack, but I don't have enough recent experience with Webpack to recommend any solution for that.

If I was faced with your challenge, I would do a little experiment to see if I could get things and running with System.js, as it can consume all three module systems. This would allow you to refactor the module parts of the code slowly, instead of porting the entire codebase in one go, and risking de-stabilising the whole system.

Another tool that might be useful in your arsenal: codemod can help you rewrite all the AMD code to CommonJS/ESM, including the code for the tests.

@fatso83 It appears that my basic test was failing because of the modules setting in babel. I had it set to false which told Babel not to convert the module format. The default transforms modules into CommonJS format, which appears to be why your sample repo with babel worked.

Since tree-shaking with Webpack doesn't work on modules without a static structure, the recommended setting is to disable module transforming in Babel (as long as you are using ESM syntax). So most people using webpack and babel will likely see the same issue as I did.

@ctaylo21 Maybe you are running Babel 5, because that setting is no longer supported. Enabling it in my repo gives this:
ReferenceError: [BABEL] src/mod1.js: Using removed Babel 5 option: foreign.modules - Use the corresponding module transform plugin in thepluginsoption. Check out http://babeljs.io/docs/plugins/#modules

These days it seems one has to explicitly enable a certain transform, but I am not sure how to dig into this further ...

Hm, I thought I had the latest packages with Babel. Might be a combination of packages or something. Either way, it's fine to drop this. I understand why my issue happened and why babel can possibly "fix it" because it can transform modules to different formats. I sincerely appreciate all the help and advice.

I am facing the same issue.
a.ts:

export function funcA(args) {
  return function c() {};
}

funB.ts:

import * as a from './a';

export const funcB = () => a.funcA({ arg: 123 });

funcB.test.ts:

import { expect } from 'chai';
import sinon from 'sinon';
import * as a from './a';
import { funcB } from './funcB';

describe('57796168', () => {
  let sandbox: sinon.SinonSandbox;
  before(() => {
    sandbox = sinon.createSandbox();
  });
  it('should call a.funcA', () => {
    const funcAStub = sandbox.stub(a, 'funcA');
    funcB();
    expect(funcAStub.calledWith({ args: 123 })).to.be.true;
  });
});

Unit test result:

  57796168
    1) should call a.funcA


  0 passing (16ms)
  1 failing

  1) 57796168
       should call a.funcA:

      AssertionError: expected false to be true
      + expected - actual

      -false
      +true

      at Context.<anonymous> (src/stackoverflow/57796168/funcB.test.ts:14:54)

@mrdulin This is a closed issue, meaning it has been resolved. It is also unclear _what_ you are also seeing, explicitly, as it's not clear which environment you are running the code in (pure Node, Babel transforms, Webpack, ... etc).

If you need some assistance in getting things working, please post it to StackOverflow and tag it with sinon, so the bigger community can help answer your questions.

If you feel that your topic is an actual _new_ issue with Sinon, please open a new ticket and follow the guidelines for reporting an issue.

Before you do that, please read through this thread carefully and try to understand what Sinon actually can do something about, and what is otherwise outside the scope of Sinon (such as intricacies caused by your module bundler, like Webpack or Babel).

Was this page helpful?
0 / 5 - 0 ratings

Related issues

zimtsui picture zimtsui  ·  3Comments

JakobJingleheimer picture JakobJingleheimer  ·  3Comments

OscarF picture OscarF  ·  4Comments

ALeschinsky picture ALeschinsky  ·  4Comments

fearphage picture fearphage  ·  3Comments