How to test an ES6 class that needs jquery?

I have an ES6 module that needs jquery.

import $ from 'jquery';

export class Weather {
    /**
     * Constructor for Weather class
     *
     * @param latitude
     * @param longitude
     */
    constructor(latitude, longitude) {
        this.latitude  = latitude;
        this.longitude = longitude;
    }

    /**
     * Fetches the weather using API
     */
    getWeather() {
        return $.ajax({
            url: 'http://localhost:8080/weather?lat=' + this.latitude + '&lon=' + this.longitude,
            method: "GET",
        }).promise();
    }
}

Module works fine when use it in my main module but the issue is with the test that I am writing for it.

Here's the test:

import {Weather} from '../js/weather';
import chai from 'chai';
import sinon from 'sinon';

chai.should();

describe('weatherbot', function() {
    beforeEach(function() {
        this.xhr = sinon.useFakeXMLHttpRequest();

        this.requests = [];
        this.xhr.onCreate = function(xhr) {
            this.requests.push(xhr);
        }.bind(this);
    });

    afterEach(function() {
        this.xhr.restore();
    });

    it('should return a resolved promise if call is successful', (done) => {
        let weather = new Weather(43.65967339999999, -79.72506369999999);

        let data = '{"coord":{"lon":-79.73,"lat":43.66},"weather":[{"id":521,"main":"Rain","description":"shower rain","icon":"09d"}],"base":"stations","main":{"temp":15.28,"pressure":1009,"humidity":82,"temp_min":13,"temp_max":17},"visibility":24140,"wind":{"speed":7.2,"deg":30},"clouds":{"all":90},"dt":1496770020,"sys":{"type":1,"id":3722,"message":0.0047,"country":"CA","sunrise":1496741873,"sunset":1496797083},"id":5907364,"name":"Brampton","cod":200}';

        weather.getWeather().then((data) => {
            expect(data.main.temp).to.equal(15.28);
            done();
        });

        this.requests[0].respond("GET", "/weather?lat=43.659673399999996&lon=-79.72506369999999", [
            200, {"Content-Type":"application/json"}, JSON.stringify(data)
        ]);
    });
});

And here's my package.json:

{
  "devDependencies": {
    "babel-core": "^6.24.1",
    "babel-loader": "^6.1.0",
    "babel-polyfill": "^6.3.14",
    "babel-preset-es2015": "^6.1.18",
    "chai": "^3.5.0",
    "copy-webpack-plugin": "^0.2.0",
    "css-loader": "^0.28.0",
    "extract-text-webpack-plugin": "^2.1.0",
    "file-loader": "^0.11.1",
    "mocha": "^3.4.1",
    "mocha-webpack": "^1.0.0-beta.1",
    "qunitjs": "^2.3.2",
    "sinon": "^2.2.0",
    "style-loader": "^0.16.1",
    "svg-inline-loader": "^0.7.1",
    "webpack": "*",
    "webpack-dev-server": "^1.12.1",
    "webpack-node-externals": "^1.6.0"
  },
  "scripts": {
    "build": "webpack",
    "watch": "webpack --watch --display-error-details",
    "start": "webpack-dev-server --hot --inline --port 8383",
    "test": "mocha --compilers js:babel-core/register ./test/*.js",
    "test:watch": "npm run test -- --watch"
  },
  "babel": {
    "presets": [
      "es2015"
    ]
  },
  "dependencies": {
    "bootstrap": "^3.3.7",
    "jquery": "^3.2.1",
    "webpack": "*"
  }
}

As you can see I only have to do npm test to run the test.

When do npm test, I get this error:

TypeError: _jquery2.default.ajax is not a function
      at Weather.getWeather (js/weather.js:19:18)
      at Context.<anonymous> (test/index.js:26:17)

But I am importing the jquery in the module, why it might be happening?


Solution 1:

There are two main problems here. The first is of course that you need to fix your import problem, but that is unrelated to testing. You will need to resolve this before going into testing, and this might have to do with the configuration of your build tool versus running in Node. You should open a separate question for this, although this might be of help. Probably all you need to do is replace the import with this import * as jQuery from 'jquery';

The other big issue is that you are running it inside of Node (using npm test that triggers Mocha) while your code requires a browser. The fake server implementation of Sinon is meant to be used in a browser environment, and you are running the tests in a server environment. That means neither jQuery nor the fake server setup will work, as Node does not have a XHR object.

So although the Sinon XHR setup seems fine, unless you are willing to change your test runner to run your tests inside of a browser environment (Karma is great for doing this from the CLI!), you need to handle this in another way. I seldom reach for faking XHR, and instead I stub out dependencies at a higher level. The answer from @CarlMarkham is touching upon this, but he does not go into details on how this would work with your code.

You are basically left with two options when running your code in Node:

  1. Intercept calls that import the JQuery module and replace it with your own object that has a stubbed version of ajax. This requires a module loader intercepter such as rewire or proxyquire.
  2. Use dependency injection directly in your module.

The Sinon homepage has a good article by Morgan Roderick on the first option, as well as several links to other articles elsewhere on the net, but no how to that explains how to do the first option. I should write one when I have time ... but here goes:

Using dependency injection on the instance level

The least invasive way is to just expose the ajax method on the instance you are testing. That means you would not need to inject anything into the module itself, and you don't have to think about cleanup afterwards:

// weather.js
export class Weather {
    constructor(latitude, longitude) {
        this.ajax = $.ajax;
        this.latitude  = latitude;
        this.longitude = longitude;
    }

    getWeather() {
        return this.ajax({ ...

// weather.test.js

it('should return a resolved promise if call is successful', (done) => {
    const weather = new Weather(43.65, -79.725);
    const data = '{"coord":{"lon":-79.73, ... }' // fill in
    weather.ajax = createStub(data);

I have written a more elaborate example of this technique on the Sinon issue tracker.

There is another way, that is more invasive, but lets you keep the class code unaltered by directly modifying the dependencies of the module:

Using dependency injection on the module level

Just modify your Weather class to export a setter interface for your dependencies so that they can be overwritten:

export const __setDeps(jQuery) => $ = jQuery;

Now you can simplify your test to read like this:

import weather from '../js/weather';
const Weather = weather.Weather;

const fakeJquery = {};
weather.__setDeps(fakeQuery);

const createStub = data => () => { promise: Promise.resolve(data) };

it('should return a resolved promise if call is successful', (done) => {
    const weather = new Weather(43.65, -79.725);
    const data = '{"coord":{"lon":-79.73, ... }' // fill in
    fakeQuery.ajax = createStub(data);

    weather.getWeather().then((data) => {
        expect(data.main.temp).to.equal(15.28);
        done();
    });
}

One problem with this approach is that you are tampering with the internals of the module, and so you need to restore the jQuery object in case you need to use the Weather class in other tests. You could of course also do the inverse: instead of injecting a fake jQuery object you can export the actual jQuery object and modify the ajax method directly. You would then delete all the injection code in the sample code above and modify it to read something like

// weather.js
export const __getDependencies() => { jquery: $ };


// weather.test.js

it('should return a resolved promise if call is successful', (done) => {
    const weather = new Weather(43.65, -79.725);
    const data = '{"coord":{"lon":-79.73, ... }' // fill in
    
    __getDependencies().jquery.ajax = createStub(data);

     // do test
     
     // restore ajax on jQuery back to its original state