180

I have the following ES6 modules:

File network.js

export function getDataFromServer() {
  return ...
}

File widget.js

import { getDataFromServer } from 'network.js';

export class Widget() {
  constructor() {
    getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }

  render() {
    ...
  }
}

I'm looking for a way to test Widget with a mock instance of getDataFromServer. If I used separate <script>s instead of ES6 modules, like in Karma, I could write my test like:

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(window, "getDataFromServer").andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

However, if I'm testing ES6 modules individually outside of a browser (like with Mocha + Babel), I would write something like:

import { Widget } from 'widget.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(?????) // How to mock?
    .andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

Okay, but now getDataFromServer is not available in window (well, there's no window at all), and I don't know a way to inject stuff directly into widget.js's own scope.

So where do I go from here?

  1. Is there a way to access the scope of widget.js, or at least replace its imports with my own code?
  2. If not, how can I make Widget testable?

Stuff I considered:

a. Manual dependency injection.

Remove all imports from widget.js and expect the caller to provide the deps.

export class Widget() {
  constructor(deps) {
    deps.getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }
}

I'm very uncomfortable with messing up Widget's public interface like this and exposing implementation details. No go.


b. Expose the imports to allow mocking them.

Something like:

import { getDataFromServer } from 'network.js';

export let deps = {
  getDataFromServer
};

export class Widget() {
  constructor() {
    deps.getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }
}

then:

import { Widget, deps } from 'widget.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(deps.getDataFromServer)  // !
      .andReturn("mockData");
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

This is less invasive, but it requires me to write a lot of boilerplate for each module, and there's still a risk of me using getDataFromServer instead of deps.getDataFromServer all the time. I'm uneasy about it, but that's my best idea so far.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Kos
  • 70,399
  • 25
  • 169
  • 233
  • If there is no _native_ mock support for this kind of import I would probably think over writing an own transformer for babel converting your ES6 style import to a custom mockable import system. This for sure would add another layer of possible failure and changes the code you want to test, ... . – t.niese Feb 06 '16 at 11:57
  • 1
    I can't set a test suite right now, but I'd try to use jasmin's `createSpy` (https://github.com/jasmine/jasmine/blob/375a6f9fda57fdd896acce9abba7aca2e02b310a/src/core/base.js#L64) function with an imported reference to getDataFromServer from 'network.js' module. So that, in the widget's tests file you'd import getDataFromServer, and then would `let spy = createSpy('getDataFromServer', getDataFromServer)` – Microfed Feb 06 '16 at 12:13
  • The second guess is to return an object from 'network.js' module, not a function. In that way, you could `spyOn` on that object, imported from `network.js` module. It's always a reference to the same object. – Microfed Feb 06 '16 at 12:15
  • Actually, it's already an object, from what I can see: http://babeljs.io/repl/#?experimental=false&evaluate=true&loose=false&spec=false&code=import%20%7B%20funcName%20%7D%20from%20'module.js'%3B%0A%0AfuncName()%3B%0A%0Aexport%20function%20getDataFromServer()%20%7B%0A%20%20return%200%3B%0A%7D – Microfed Feb 06 '16 at 12:20
  • Well, the main problem is with that approach you'd need to import the whole `network` module (object) and make call like that: `network.getDataFromServer()`, not just `getDataFromServer()`. :( – Microfed Feb 06 '16 at 12:27
  • Possible duplicate of [How to mock dependencies for unit tests with ES6 Modules](http://stackoverflow.com/questions/27323031/how-to-mock-dependencies-for-unit-tests-with-es6-modules) – just-boris Feb 08 '16 at 19:18
  • 2
    I don't really understand how dependency injection messes up `Widget`'s public interface? `Widget` is messed up *without* `deps`. Why not make the dependency explicit? – thebearingedge Jul 23 '16 at 23:14
  • Duplicate of this: https://stackoverflow.com/questions/27323031/how-to-mock-dependencies-for-unit-tests-with-es6-modules – superluminary Mar 06 '18 at 09:54

12 Answers12

145

I've started employing the import * as obj style within my tests, which imports all exports from a module as properties of an object which can then be mocked. I find this to be a lot cleaner than using something like rewire or proxyquire or any similar technique. I've done this most often when needing to mock Redux actions, for example. Here's what I might use for your example above:

import * as network from 'network.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(network, "getDataFromServer").andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});

If your function happens to be a default export, then import * as network from './network' would produce {default: getDataFromServer} and you can mock network.default.

Note: the ES spec defines modules as read-only, and many ES transpilers have started honoring this, which may break this style of spying. This is highly dependent on your transpiler as well as your test framework. For example, I think Jest performs some magic to make this work, though Jasmine does not, at least currently. YMMV.

carpeliam
  • 6,691
  • 2
  • 38
  • 42
  • 3
    Do you use the `import * as obj` only in the test or also in your regular code? – Chau Thai Sep 19 '16 at 09:31
  • I usually use this pattern only in my test- in my app code, I usually use `import { thing1, thing2 } from 'thinglib';`. – carpeliam Sep 20 '16 at 12:35
  • 61
    @carpeliam This wont work with the ES6 module spec where the imports are readonly. – ashish Apr 18 '17 at 02:26
  • 12
    Jasmine is complaining `[method_name] is not declared writable or has no setter` which makes sense since es6 imports are constant. Is there a way to workaround? – lpan May 11 '17 at 18:11
  • @lpan If you mean to say that es6 imports can not be reassigned, this is correct, but es6 constants don't protect from child modification, which is what spying ultimately does. All of this requires that your properties are writable though (which I think is what ashish is getting at in his comment above) - see http://stackoverflow.com/q/7757337/242582. – carpeliam May 17 '17 at 23:47
  • Thanks a lot. This one was really difficult for me to find! The import as made the trick – danivicario Sep 25 '17 at 16:38
  • Thank you for posting this, @carpeliam. It is very helpful. A question though, is there any way to imoprt the system under test multiple times, such as in a beforeEach block? The reason being tests can mess up state of the system under test for other tests that follow. – Francisc Nov 17 '17 at 13:20
  • 3
    @Francisc `import` (unlike `require`, which can go anywhere) gets hoisted, so you can’t technically import multiple times. Sounds like your spy is being called elsewhere? In order to keep the tests from messing up state (known as test pollution), you can reset your spies in an afterEach (e.g. sinon.sandbox). Jasmine I believe does this automatically. – carpeliam Nov 18 '17 at 14:33
  • @carpeliam Thank you for your comment. I meant something like the fiddle I linked at the end. Without ability to reimport the system under test, it doesn't seem possible to test that because once the promise stored in `ready` settles, you can't change it. Am I wrong? https://jsfiddle.net/w9x3bp8r/2/ – Francisc Nov 19 '17 at 20:49
  • 1
    @carpeliam Here's another example I thought of https://jsfiddle.net/b93L16nz/. Code is for illustration as well. The non-exported `response` variable, once set, cannot be changed. So if I want to test both success and fail situations, I cannot without reimporting the system under test. I can add methods to allow me to achieve that, but I don't like changing implementation to accommodate unit tests generally. – Francisc Nov 19 '17 at 21:32
  • when doing this `import * as network from 'network.js';` then `network` object is frozen right ? so how does `spyOn(network, "getDataFromServer").andReturn("mockData")` modify the `getDataFromServer` method ? – agent47 Mar 08 '18 at 21:49
  • @agent47 the object is not necessarily frozen - some modules will freeze their exports, but many will not. This is what ashish gets at in an above comment. – carpeliam Mar 11 '18 at 17:05
  • @carpeliam i read here http://exploringjs.com/es6/ch_modules.html#sec_imports-as-views-on-exports (search for frozen object) that when importing with `*` the object is frozen by default (readonly), so is that not true ? – agent47 Mar 12 '18 at 18:50
  • 16
    @agent47 The problem is that while the ES6 spec specifically prevents this answer from working, in exactly the way you mentioned, most people who write `import` in their JS aren't really using ES6 modules. Something like webpack or babel will step in at build-time and convert it either into their own internal mechanism for calling distant parts of the code (eg `__webpack_require__`) or into one of the pre-ES6 *de facto* standards, CommonJS, AMD or UMD. And that conversion often doesn't adhere strictly to the spec. So for many, many devs just now, this answer works fine. For now. – daemone May 22 '18 at 16:06
39

carpeliam is correct, but note that if you want to spy on a function in a module and use another function in that module calling that function, you need to call that function as part of the exports namespace, otherwise the spy won't be used.

Wrong example:

// File mymodule.js

export function myfunc2() {return 2;}
export function myfunc1() {return myfunc2();}

// File tests.js
import * as mymodule

describe('tests', () => {
    beforeEach(() => {
        spyOn(mymodule, 'myfunc2').and.returnValue = 3;
    });

    it('calls myfunc2', () => {
        let out = mymodule.myfunc1();
        // 'out' will still be 2
    });
});

Right example:

export function myfunc2() {return 2;}
export function myfunc1() {return exports.myfunc2();}

// File tests.js
import * as mymodule

describe('tests', () => {
    beforeEach(() => {
        spyOn(mymodule, 'myfunc2').and.returnValue = 3;
    });

    it('calls myfunc2', () => {
        let out = mymodule.myfunc1();
        // 'out' will be 3, which is what you expect
    });
});
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
vdloo
  • 582
  • 6
  • 5
  • 6
    I wish i could up vote this answer 20 more times! Thank you! – sfletche Apr 11 '17 at 23:23
  • Can someone explain why this is the case? Is exports.myfunc2() a copy of myfunc2() without being a direct reference? – Colin Whitmarsh Aug 17 '17 at 16:45
  • 2
    @ColinWhitmarsh `exports.myfunc2` is a direct reference to `myfunc2` until `spyOn` replaces it with a reference to a spy function. `spyOn` will change the value of `exports.myfunc2` and replace it with a spy object, whereas `myfunc2` remains untouched in the module's scope (because `spyOn` has no access to it) – madprog Nov 27 '17 at 10:17
  • 1
    shouldn't importing with `*` freeze the object and the object attributes cannot be changed ? – agent47 Mar 08 '18 at 23:33
  • 2
    Just a note that this recommendation of using `export function` along with `exports.myfunc2` is technically mixing commonjs and ES6 module syntax and this is not allowed in newer versions of webpack (2+) that require all-or-nothing ES6 module syntax usage. I added an answer below based on this one that will work in ES6 strict environments. – QuarkleMotion May 19 '18 at 19:05
9

vdloo's answer got me headed in the right direction, but using both CommonJS "exports" and ES6 module "export" keywords together in the same file did not work for me (Webpack v2 or later complains).

Instead, I'm using a default (named variable) export wrapping all of the individual named module exports and then importing the default export in my tests file. I'm using the following export setup with Mocha/Sinon and stubbing works fine without needing rewire, etc.:

// MyModule.js
let MyModule;

export function myfunc2() { return 2; }
export function myfunc1() { return MyModule.myfunc2(); }

export default MyModule = {
  myfunc1,
  myfunc2
}

// tests.js
import MyModule from './MyModule'

describe('MyModule', () => {
  const sandbox = sinon.sandbox.create();
  beforeEach(() => {
    sandbox.stub(MyModule, 'myfunc2').returns(4);
  });
  afterEach(() => {
    sandbox.restore();
  });
  it('myfunc1 is a proxy for myfunc2', () => {
    expect(MyModule.myfunc1()).to.eql(4);
  });
});
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
QuarkleMotion
  • 814
  • 8
  • 10
  • Helpful answer, thanks. Just wanted to mention that the `let MyModule` isn't required to use the default export (it can be a raw object). Also, this method doesn't require `myfunc1()` to call `myfunc2()`, it works to just spy on it directly. – Mark Edington Aug 05 '18 at 18:33
  • @QuarkleMotion: It appears you edited this with a different account than your main account by accident. That's why your edit had to go through a manual approval -- it didn't look like it was from *you* I assume this was just an accident, but, if it was intentional, you should [read the official policy on sock puppet accounts so you don't accidentally violate the rules](https://meta.stackexchange.com/questions/57682/how-should-sockpuppets-be-handled-on-stack-exchange). – Conspicuous Compiler May 08 '19 at 23:47
  • 1
    @ConspicuousCompiler thanks for the heads up - this was a mistake, I did not intend to modify this answer with my work email-linked SO account. – QuarkleMotion May 22 '19 at 03:12
  • This seems to be an answer to a different question! Where's widget.js and network.js? This answer seems to have no transitive dependency, which is what made the original question hard. – Bennett McElwee May 28 '20 at 04:01
8

I implemented a library that attempts to solve the issue of run time mocking of TypeScript class imports without needing the original class to know about any explicit dependency injection.

The library uses the import * as syntax and then replaces the original exported object with a stub class. It retains type safety so your tests will break at compile time if a method name has been updated without updating the corresponding test.

This library can be found here: ts-mock-imports.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
EmandM
  • 922
  • 6
  • 12
3

I have found this syntax to be working:

My module:

// File mymod.js
import shortid from 'shortid';

const myfunc = () => shortid();
export default myfunc;

My module's test code:

// File mymod.test.js
import myfunc from './mymod';
import shortid from 'shortid';

jest.mock('shortid');

describe('mocks shortid', () => {
  it('works', () => {
    shortid.mockImplementation(() => 1);
    expect(myfunc()).toEqual(1);
  });
});

See the documentation.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
nerfologist
  • 761
  • 10
  • 23
  • +1 and with some additional instructions: Seems to only work with node modules i.e. stuff that you have on package.json. And more important is, something that is not mentioned in the Jest docs, the string passed to `jest.mock()` has to match the name used in import/packge.json instead of the name of constant. In the docs they are both same, but with code like `import jwt from 'jsonwebtoken'` you need setup the mock as `jest.mock('jsonwebtoken')` – kaskelotti May 24 '19 at 13:07
1

I haven't tried it myself, but I think mockery might work. It allows you to substitute the real module with a mock that you have provided. Below is an example to give you an idea of how it works:

mockery.enable();
var networkMock = {
    getDataFromServer: function () { /* your mock code */ }
};
mockery.registerMock('network.js', networkMock);

import { Widget } from 'widget.js';
// This widget will have imported the `networkMock` instead of the real 'network.js'

mockery.deregisterMock('network.js');
mockery.disable();

It seems like mockery isn't maintained anymore and I think it only works with Node.js, but nonetheless, it's a neat solution for mocking modules that are otherwise hard to mock.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Erik B
  • 40,889
  • 25
  • 119
  • 135
1

I recently discovered babel-plugin-mockable-imports which handles this problem neatly, IMHO. If you are already using Babel, it's worth looking into.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Dominic P
  • 2,284
  • 2
  • 27
  • 46
1

See suppose I'd like to mock results returned from isDevMode() function in order to check to see how code would behave under certain circumstances.

The following example is tested against the following setup

    "@angular/core": "~9.1.3",
    "karma": "~5.1.0",
    "karma-jasmine": "~3.3.1",

Here is an example of a simple test case scenario

import * as coreLobrary from '@angular/core';
import { urlBuilder } from '@app/util';

const isDevMode = jasmine.createSpy().and.returnValue(true);

Object.defineProperty(coreLibrary, 'isDevMode', {
  value: isDevMode
});

describe('url builder', () => {
  it('should build url for prod', () => {
    isDevMode.and.returnValue(false);
    expect(urlBuilder.build('/api/users').toBe('https://api.acme.enterprise.com/users');
  });

  it('should build url for dev', () => {
    isDevMode.and.returnValue(true);
    expect(urlBuilder.build('/api/users').toBe('localhost:3000/api/users');
  });
});

Exemplified contents of src/app/util/url-builder.ts

import { isDevMode } from '@angular/core';
import { environment } from '@root/environments';

export function urlBuilder(urlPath: string): string {
  const base = isDevMode() ? environment.API_PROD_URI ? environment.API_LOCAL_URI;

  return new URL(urlPath, base).toJSON();
}
nakhodkin
  • 1,327
  • 1
  • 17
  • 27
1

You can use putout-based library mock-import for this purpose.

Let's suppose you have a code you want to test, let it be cat.js:

import {readFile} from 'fs/promises';

export default function cat() {
    const readme = await readFile('./README.md', 'utf8');
    return readme;
};

And tap-based test with a name test.js will look this way:

import {test, stub} from 'supertape';
import {createImport} from 'mock-import';

const {mockImport, reImport, stopAll} = createMockImport(import.meta.url);

// check that stub called
test('cat: should call readFile', async (t) => {
    const readFile = stub();
    
    mockImport('fs/promises', {
        readFile,
    });
    
    const cat = await reImport('./cat.js');
    await cat();
    
    stopAll();
    
    t.calledWith(readFile, ['./README.md', 'utf8']);
    t.end();
});

// mock result of a stub
test('cat: should return readFile result', async (t) => {
    const readFile = stub().returns('hello');
    
    mockImport('fs/promises', {
        readFile,
    });
    
    const cat = await reImport('./cat.js');
    const result = await cat();
    
    stopAll();
    
    t.equal(result, 'hello');
    t.end();
});

To run test we should add --loader parameter:

node --loader mock-import test.js

Or use NODE_OPTIONS:

NODE_OPTIONS="--loader mock-import" node test.js

On the bottom level mock-import uses transformSource hook, which replaces on the fly all imports with constants declaration to such form:

const {readFile} = global.__mockImportCache.get('fs/promises');

So mockImport adds new entry into Map and stopAll clears all mocks, so tests not overlap.

All this stuff needed because ESM has it's own separate cache and userland code has no direct access to it.

coderaiser
  • 749
  • 5
  • 15
1

I resolved it by adding it to the class and then overwriting it, because this lib was not allowing me to do anything with their getter :'(

import { libFn } from 'lib';

class MyComponent {
  libFn = libFn;

  inUse() {
    this.libFn();
  }
}
it('', () => {
  component.libFn = jasmine.createSpy().and.returnValue(5);
});

That's how easy it is. I'm pretty sure that's not the best way, because the libFn should not be part of my class. Jasmine should learn from Jest by mocking libs...

Duba
  • 78
  • 7
0

Here is an example to mock an imported function

File network.js

export function exportedFunc(data) {
  //..
}

File widget.js

import { exportedFunc } from 'network.js';

export class Widget() {
  constructor() {
    exportedFunc("data")
  }
}

Test file

import { Widget } from 'widget.js';
import { exportedFunc } from 'network'
jest.mock('network', () => ({
  exportedFunc: jest.fn(),
}))

describe("widget", function() {
  it("should do stuff", function() {
    let widget = new Widget();
    expect(exportedFunc).toHaveBeenCalled();
  });
});
seng
  • 1
  • Jest seem not yet ready for esm. https://stackoverflow.com/questions/71373579/unit-test-mock-node-17-typescript-esm-module#comment126160658_71373579 – Wajahath Mar 07 '22 at 01:53
0

I haven't been able to try it out yet, but (Live demo at codesandbox.io/s/adoring-orla-wqs3zl?file=/index.js)

If you have a browser-based test runner in theory you should be able to include a Service Worker that can intercept the request for the ES6 module you want to mock out and replace it with an alternative implementation (similar or the same as how Mock Service Worker approaches things)

So something like this in your service worker

self.addEventListener('fetch', (event) => {
  if (event.request.url.includes("canvas-confetti")) {
      event.respondWith(
        new Response('const confetti=function() {}; export default confetti;', {
          headers: { 'Content-Type': 'text/javascript' }
        })
      );
    }
});

If your source code is pulling in an ES6 module like this

import confetti from 'https://cdn.skypack.dev/canvas-confetti';
confetti();
grunet
  • 311
  • 2
  • 9
  • This would only work if `getDataFromServer` is using [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) or importing from a URL, right? (not for WebSockets, dynamic `import` passed through [`webpack`](https://webpack.js.org/), etc.) – jacobq Apr 06 '22 at 22:03
  • Yeah only stuff a service worker can intercept, so no websockets for sure. Dynamic import I'm guessing would still be fine though (or at least I can't think how that'd differ too much from the static case). Wish there was an easy way to try this in an online sandbox and link to it – grunet Apr 08 '22 at 01:43
  • 1
    Found out CodeSandbox lets you stick in a service worker in their Node server option. Made a demo with that of this concept here - https://codesandbox.io/s/adoring-orla-wqs3zl?file=/index.js (I was finding unregistering the service worker between edits tough, so I just forked the sandbox each time for that...) – grunet Apr 10 '22 at 04:49