13

I'm using AVA + sinon to build my unit test. Since I need ES6 modules and I don't like babel, I'm using mjs files all over my project, including the test files. I use "--experimental-modules" argument to start my project and I use "esm" package in the test. The following is my ava config and the test code.

  "ava": {
    "require": [
      "esm"
    ],
    "babel": false,
    "extensions": [
      "mjs"
    ]
  },


// test.mjs
import test from 'ava';
import sinon from 'sinon';
import { receiver } from '../src/receiver';
import * as factory from '../src/factory';

test('pipeline get called', async t => {
  const stub_factory = sinon.stub(factory, 'backbone_factory');
  t.pass();
});

But I get the error message:

  TypeError {
    message: 'ES Modules cannot be stubbed',
  }

How can I stub an ES6 module without babel?

oligofren
  • 20,744
  • 16
  • 93
  • 180
Jim Jin
  • 1,249
  • 1
  • 15
  • 28

3 Answers3

6

According to John-David Dalton, the creator of the esm package, it is only possible to mutate the namespaces of *.js files - *.mjs files are locked down.

That means Sinon (and all other software) is not able to stub these modules - exactly as the error message points out. There are two ways to fix the issue here:

  1. Just rename the files' extension to .js to make the exports mutable. This is the least invasive, as the mutableNamespace option is on by default for esm. This only applies when you use the esm loader, of course.
  2. Use a dedicated module loader that proxies all the imports and replaces them with one of your liking.

The tech stack agnostic terminology for option 2 is a link seam - essentially replacing Node's default module loader. Usually one could use Quibble, ESMock, proxyquire or rewire, meaning the test above would look something like this when using Proxyquire:

// assuming that `receiver` uses `factory` internally

// comment out the import - we'll use proxyquire
// import * as factory from '../src/factory';
// import { receiver } from '../src/receiver';

const factory = { backbone_factory: sinon.stub() };
const receiver = proxyquire('../src/receiver', { './factory' : factory });

Modifying the proxyquire example to use Quibble or ESMock (both supports ESM natively) should be trivial.

oligofren
  • 20,744
  • 16
  • 93
  • 180
  • Still get 'ES Modules cannot be stubbed'. I can make a runnable project. But how can I send the project to you? – Jim Jin Jun 09 '18 at 01:12
  • @JimJin Create a public github repo. If you make the absolute _minimal_ project (containing only one file to import and one test file, for instance) I only need to clone it. An example repo: https://github.com/fatso83/sinon-1711-babel – oligofren Jun 09 '18 at 07:11
  • @JimJin I just realized you chose to not fill in the bug report template on Sinon. That was unfortunate, as it means I don't know which version of Sinon you are using. I have up until now thought you were using the absolute latest version, but it seems you might be on an older version? Support for stubbing ESM modules using the option mentioned above was not added until [Sinon 5.0.8](https://github.com/sinonjs/sinon/blob/master/History.md#508--2018-05-24) in [PR 1803](https://github.com/sinonjs/sinon/pull/1803). – oligofren Jun 09 '18 at 07:17
  • I'm using the latest sinon. I'll create the repo ASAP. – Jim Jin Jun 10 '18 at 13:45
  • See updated answer. This isn't currently possible using only Sinon: you'll need to employ module loading middleware such as `proxyquire` or `rewire` to intercept the module loading. – oligofren Jun 11 '18 at 06:06
  • I'd like to write unit test with the minimum cost. If the solution would be so tricky, I'd rather abandon the unit test with mock. After all, mock test is not the imperative method to build a robust online system. – Jim Jin Jun 11 '18 at 07:13
  • But, as a last resort, is it possible that sinon wraps the "proxyrequire" (or something like this) for me ? – Jim Jin Jun 11 '18 at 09:52
  • Feature requests is outside the scope of the question you asked by some margin :-) But generally speaking, having a module loader magically intercepting module calls is not what people want. It will modify your code in weird and mysterious ways when you don't control the loading. Using link level loaders is a very common thing in any case, as it's impossible to control module dependencies without it - unless you employ some kind of IOC/dependency injection scheme, so you might as well look into it. Try reading up on the link given in the answer to understand what it does and how. – oligofren Jun 11 '18 at 14:23
  • I'll investigate IOC on node. Thank you very much. – Jim Jin Jun 12 '18 at 03:29
  • The up/down arrows on the answer :-) – oligofren Jun 12 '18 at 06:25
  • Clicked! The first time I know that is is clickable! – Jim Jin Jun 12 '18 at 06:27
  • 1
    I can't change the number by clicking the upvote. That is sad. – Jim Jin Jun 12 '18 at 06:29
  • node now supports ESM natively. I am not using any extra package and config for ESM import/export in my node project. I just need to add ""type": "module" and node allow ESM syntax and all files have '.js' extension. Now I am trying to stub using `sinon` but getting the error `ES Modules cannot be spied`. any suggestion? or look like I need to add a new question about this? – Kiran Mali Feb 04 '22 at 13:06
  • 1
    Node has supported ESM natively for many years, Kiran. Enabling that option in package.json just changes how it handles the js extension so that it behaves the same as mjs. Everything in my answer still applies. – oligofren Feb 04 '22 at 16:57
  • Apart from the option of using a custom module loader in your tests (i.e. a link seam), a good alternative is refactoring your code slightly too allow for injecting dependencies. See https://stackoverflow.com/a/70903374/200987 – oligofren Feb 04 '22 at 17:00
  • Alternatively https://stackoverflow.com/a/70895476 – oligofren Feb 04 '22 at 17:02
  • thanks, oligofren, I tried the first option `Just rename the files' extension to .js to make the exports mutable` but it is not working. Are you suggesting me to use the second `dedicated module loader ` option? – Kiran Mali Feb 07 '22 at 10:16
  • 1
    @KiranMali You missed an essential bit. This answer uses the `esm` package. That quote is in context of that. You are not using the `esm` package. Unless you are using something that changes the runtime into something not ESM-compliant, which is what Jest does, you either need to refactor your code to something cleaner to make DI possible, or employ something that hooks into the module loading at runtime. Which are what those two links suggest. – oligofren Feb 07 '22 at 12:42
  • now got it. thanks again mate oligofren ! – Kiran Mali Feb 07 '22 at 14:11
1

Sinon needs to evolve with the times or be left behind (ESM is becoming defacto now with Node 12) as it is turning out to be a giant pain to use due to its many limitations.

This article provides a workaround (actually 4, but I only found 1 to be acceptable). In my case, I was exporting functions from a module directly and getting this error: ES Modules cannot be stubbed

export function abc() {

}

The solution was to put the functions into a class and export that instead:

export class Utils {
  abc() {
  
  }
}

notice that the function keyword is removed in the method syntax.

Happy Coding - hope Sinon makes it in the long run, but it's not looking good given its excessive rigidity.

java-addict301
  • 3,220
  • 2
  • 25
  • 37
  • This has nothing to do with Sinon. It is per the ES 2015 spec. It is like complaining that no one has made it possible to do 1+1 = 3. You can only do something about this if you control the runtime. Jest does this by intercepting module calls, but Jest is also much more than a stubbing library; it is a test runtime, framework, module mocker and more. That means it also has trouble adapting to other situations. Sinon has never done module interception and leaves that to other tools, instead focusing on creating the fakes part. P.S. Stuffing this in a class is redundant. Just export an object. – oligofren Jan 26 '22 at 11:05
  • This also does not answer the question: "How can I stub an ES6 module without babel?". You are still not stubbing an ES6 module. The same module is exported (unstubbed), and it just provides a mutable object. This is a solution to a different problem: how can you generally work around the fact that ES2015+ creates immutable module exports. – oligofren Jan 26 '22 at 11:09
  • @oligofren how can users stub ES6 modules with Sinon then? It sounds like we can't. Since ES6 modules are the way of the future, I don't see Sinon as a viable test framework moving forward until it supports this. – java-addict301 Jan 27 '22 at 04:21
  • You are missing what Sinon is. It is _not a test framework_ and it has never been marketed as such. A test framework would be something like Jasmine (a test runner, syntax for tests, assertion library) or Jest. As it says on its homepage: _Standalone test spies, stubs and mocks for JavaScript. Works with any unit testing framework._ Additionally, it can also fake time and XHR. Module loading is not part of this. You are using the wrong tool for the job. – oligofren Jan 27 '22 at 04:48
  • 2
    I am hard pressed to find _anything_ supporting this at the moment. Jest does not support native ESM either. Track issues [4842](https://github.com/facebook/jest/issues/4842) and [9430](https://github.com/facebook/jest/issues/9430) for how complex this is. Simen is doing a lot of great work at the moment to patch the module loading code, but it is super complex and hard to get right. If you need this "working", then use something that transpiles your code using Babel to CJS or use a dedicated loader like [ESMock](https://www.npmjs.com/package/esmock) – oligofren Jan 27 '22 at 04:59
  • thanks for the suggestions and info @oligofren – java-addict301 Jan 27 '22 at 06:33
  • You are most welcome <3 – oligofren Jan 27 '22 at 08:13
0

Sticking with the questions Headline „Stub an export from a native ES Module without babel“ here's my take, using mocha and esmock:

(credits: certainly @oligofren brought me on the right path…)

package.json:

  "scripts": {
      ...
      "test": "mocha --loader=esmock",

  "devDependencies": {
      "esmock": "^2.1.0",
      "mocha": "^10.2.0",

TestDad.js (a class)

import { sonBar } from './testSon.js'

export default class TestDad {
  constructor() {
    console.log(purple('constructing TestDad, calling...'))
    sonBar()
  }
}

testSon.js (a 'util' library)

export const sonFoo = () => {
  console.log(`Original Son 'foo' and here, my brother... `)
  sonBar()
}

export const sonBar = () => {
  console.log(`Original Son bar`)
}

export default { sonFoo, sonBar }

esmockTest.js

import esmock from 'esmock'

describe.only(autoSuiteName(import.meta.url),
  () => {
    it('Test 1', async() => {
      const TestDad = await esmock('../src/commands/TestDad.js', {
        '../src/commands/testSon.js': {
          sonBar: () => { console.log('STEPSON Bar') }
        }
      })

      // eslint-disable-next-line no-new
      new TestDad()
    })
    it('Test 2', async() => {
      const testSon = await esmock('../src/commands/testSon.js')

      testSon.sonBar = () => { console.log('ANOTHER STEPSON Bar') }

      testSon.sonFoo() // still original
      testSon.sonBar() // different now
    })
  })
autoSuiteName(import.meta.url)

regarding Test1

  • working nicely, import bended as desired.

regarding Test1

  • Bending a single function to do something else is not a problem. (but then there is not much test value in calling your very own function you just defined, is there?)
  • Enclosed function calls within the module (i.e. from sonFoo to sonBar) remain what they are, they are indeed a closure, still pointing to the prior function
  • Btw also tested that: No better results with sinon.callsFake() (would have been surprising if there was…)
Frank N
  • 9,625
  • 4
  • 80
  • 110