61

I'm trying to fiddle with Ecmascript 6 modules using webpack + traceur to transpile to ES5 CommonJS, but I'm having trouble successfully unit testing them.

I tried using Jest + traceur preprocessor, but the automocking and dependency names seem to get screwy, plus I can't seem to get sourceMaps to work with Jest and node-inspector debugging.

Is there a better framework to unit test ES6 modules?

Evan Layman
  • 3,691
  • 9
  • 31
  • 48
  • 2
    I don't know about Traceur, but [6to5](http://6to5.github.io/) works well with jest and has a [6to5-jest plugin](https://github.com/6to5/6to5-jest). – James Kyle Dec 05 '14 at 21:55
  • Thanks James, I will check it out. It does look like 6to5 transpiles to more human-readable and covers all of the ES6 features I would need, so maybe I will go that route. – Evan Layman Dec 08 '14 at 17:58
  • Also, were you able to get sourceMaps with jest using node-inspector to debug? – Evan Layman Dec 08 '14 at 18:43
  • 1
    I also recommend using Babel.js for transpiling your ES6 code into ES5. I haven't had any issues with it while transpiling the test or implementation code. https://babeljs.io/ – Carlos Pinto Apr 26 '15 at 09:41
  • 1
    If you are using babel then you can also simply mock modules with [babel-plugin-rewire](https://www.npmjs.com/package/babel-plugin-rewire) instead of a dedicated webpack or browserify loader. – Daniel A. R. Werner Jan 08 '16 at 13:50
  • Exactly what I was looking for @DanielA.R.Werner, thank you. – MaxPRafferty Aug 31 '16 at 22:11

5 Answers5

58

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 can't speak for traceur which was the framework used in the question, but I've found this to work with my setup of Karma, Jasmine, and Babel, and I'm posting it here as this seems to be the most popular question of this type.

I've used this strategy most often when needing to mock Redux actions. Here's a short example:

import * as exports from 'module-you-want-to-mock';
import SystemUnderTest from 'module-that-uses-above-module';

describe('your module', () => {
  beforeEach(() => {
    spyOn(exports, 'someNamedExport');  // mock a named export
    spyOn(exports, 'default');          // mock the default export
  });
  // ... now the above functions are mocked
});
carpeliam
  • 6,691
  • 2
  • 38
  • 42
  • 2
    this pattern or technique should be better documented as its a useful way to mock es6 modules rather than relying on third party dependencies. I would be interested to see as es6 becomes more mainstream that in relation to unit testing, this pattern is seen more often in examples when importing modules rather than using the `import {someExport} from module` notation. – markyph Jul 18 '16 at 19:34
  • Since you mentioned redux actions, it's worth pointing out that one has to pay close attention to when the actions are bound to a connected component. Applying spies to functions that have already been bound are likely to have no effect. Understanding the different ways to call react-redux's connect() function were key for me in getting this to work without losing my mind: https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options – killthrush Sep 20 '16 at 01:29
  • 2
    How do you keep tests from bleeding into each other? It's been my experience that mocking imports causes later tests to use the mock instead of the real instance, or vice versa. – tom Jun 16 '17 at 17:45
  • @user831865 depending on your testing framework, the scope of the mock should be the `describe` block that the mock is defined within. Hard to say though without knowing more about your particular setup. – carpeliam Jun 18 '17 at 04:17
  • 1
    @user831865 if you’re using sinon, `sinon.sandbox` can keep your mocks from bleeding into your other tests. – carpeliam Nov 18 '17 at 14:39
  • Thank you for this @carpeliam!! It's perfect for my use case! – emanuelbsilva Nov 07 '18 at 07:12
7

If you are using Webpack another option that has a little more flexibility than rewire is inject-loader.

For example, in a test that is bundled with Webpack:

describe('when an alert is dismissed', () => {

  // Override Alert as we need to mock dependencies for these tests
  let Alert, mockPubSub

  beforeEach(() => {
    mockPubSub = {}
    Alert =  require('inject!./alert')({
      'pubsub-js': mockPubSub
    }).Alert
  })

  it('should publish \'app.clearalerts\'', () => {
    mockPubSub.publish = jasmine.createSpy()
    [...]
    expect(mockPubSub.publish).toHaveBeenCalled()
  })
})

inject-loader, in a similar manner to proxyquire at least allows one to inject dependencies before importing whereas in rewire you must import first and then rewire which makes mocking some components (e.g. those that have some initialization) impossible.

djskinner
  • 8,035
  • 4
  • 49
  • 72
  • yes, but this only works when you use `require` instead `import .. as ..`syntax – alwe May 06 '16 at 11:09
  • 1
    Not true. I use this successfully with ES6 import syntax. It depends on your setup I suppose but I'm using Babel with babel-loader. It works for both `require` and ES6 imports. The only place require is needed is for mocking dependencies in the test itself. – djskinner May 06 '16 at 11:37
  • Hi @djskinner. I'm trying to get inject-loader going with babel, webpack and karam but having trouble. Would you mind adding your `karma.config.js` (and `webpack.config` file if you use one) to your answer. Thanks in advance. – Jon P Smith Jun 23 '16 at 08:12
  • Have a look at [this answer](http://stackoverflow.com/a/32176723/120497) for a good starting point. It's mainly about code coverage but the base config is good even if you're not interested in the code coverage parts. As for inject-loader I didn't need any additional config, simply `npm i inject-loader` and `require(inject!...)` – djskinner Jun 23 '16 at 08:43
  • How to use this with amd modules? I get "Module name "inject!src/qux_unnormalized2" has not been loaded yet for context" – Stefan Aug 06 '19 at 14:09
5

Hi you could use proxyquire:

import assert from 'assert';
import sinon from 'sinon';
import Proxyquire from 'proxyquire';

let proxyquire = Proxyquire.noCallThru(),
    pathModelLoader = './model_loader';

describe('ModelLoader module.', () => {
    it('Should load all models.', () => {
        let fs, modelLoader, ModelLoader, spy, path;
        fs = {
            readdirSync(path) {
                return ['user.js'];
            }
        };
        path = {
            parse(data) {
                return {name: 'user'};
            }
        };
        ModelLoader = proxyquire(pathModelLoader, {'fs': fs, 'path': path});
        modelLoader = new ModelLoader.default();
        spy = sinon.spy(modelLoader, 'loadModels');
        modelLoader.loadModels();
        assert(spy.called);
    });
});
JinVillaz
  • 51
  • 1
  • 3
3

I actually got this to work by dropping Jest and going with Karma + Jasmine + Webpack and using https://github.com/jhnns/rewire to mock dependencies

Evan Layman
  • 3,691
  • 9
  • 31
  • 48
  • 1
    Variables inside functions can not be changed by rewire. This is constrained by JavaScript and can't be circumvented by rewire. – John Wu Feb 02 '16 at 02:24
  • 3
    I'm glad this advice worked for you... but how is it the accepted answer? "Don't use that" isn't necessarily helpful :( – Steve Dec 28 '18 at 16:36
1

Proxyquire will help you, but it not gonna to work with modern webpack+ES6 modules, ie "aliases".

import fs from 'fs';
import reducers from 'core/reducers';
...
proxyquire('../module1', {
  'fs': mockFs,  // this gonna work
  'core/reducers': mockReducers // what about this?
});

that will not work. As long you can mock fs - you cannot mock reducers. You have to specify real name of dependency, after any webpack or babel transformation. Normally - name relative to module1 location. May be '../../../shared/core/reducers'. May be not.

There is drop in solutions - https://github.com/theKashey/proxyquire-webpack-alias (stable, based on fork of proxyquire) or https://github.com/theKashey/resolveQuire (less stable, can be run upon original proxyquire)

Both of them works as well, and will mock any ES6 module(they are dam good) in a proxyquire way(it is a good way)

Anton Korzunov
  • 140
  • 1
  • 8