How to test an ES6 class that needs jquery?

前端 未结 4 1845
轻奢々
轻奢々 2020-11-30 15:56

I have an ES6 module that needs jquery.

import $ from \'jquery\';

export class Weather {
    /**
     * Constructor for Weather class
     *
     * @param          


        
相关标签:
4条回答
  • 2020-11-30 15:59

    Unless it is ES6 module that has default export that is being imported (which jQuery is not), the proper way to import it is

    import * as $ from 'jquery';
    

    The way how CommonJS modules are treated with import is the responsibility of build tool. default imports for non-ES6 modules was (erroneously) supported at least in older Webpack versions. And no version restriction for

    "webpack": "*"
    

    dependency is a direct way to broken builds.

    0 讨论(0)
  • 2020-11-30 16:05

    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);
    

    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
    
    0 讨论(0)
  • 2020-11-30 16:14

    In my project, I stub all the jQuery methods I use, so, I would stub $.ajax since all you need to test are the returned promises.

    Something like this in another file:

    module.exports = {
      ajax: sinon.stub(global.$, 'ajax')
    }
    

    Then, in your test

    import { ajax } from 'stubs'
    

    This way, the ajax method is stubbed and you can define what it should return on a per test basis:

    ajax.returns({
      done: (callback) => { callback(someParam) },
      fail: () => {},
      always: (callback) => { callback() }
    });
    
    0 讨论(0)
  • 2020-11-30 16:21

    I ran into a similar problem recently. I found that when I ran a test with mocha and webpack, there was no 'window' in the scope for jquery to bind to and as a result it was undefined. To solve this problem, I found that I could follow this advice and replace import * as $ from jquery in the source file with:

    // not the winning solution
    const jsdom = require("jsdom");
    const { window } = new jsdom.JSDOM();
    const $ = require('jquery')(window);
    

    But then the source file would no longer properly bundle with webpack, so I gave up on using mocha and babel for client-side javascript tests. Instead, I found that I could test my client-side code properly using a combination of karma and phantomjs.

    First, I installed all the dependencies:

    npm install -D babel-loader @babel/core
    npm install -D mocha chai sinon mocha-webpack
    npm install -D phantomjs-prebuilt
    npm install -D webpack
    npm install -D karma karma-mocha karma-chai karma-sinon 
    npm install -D karma-mocha-reporter karma-phantomjs-launcher karma-webpack
    

    Then I setup a config file in the root called karma.config.js with:

    module.exports = function(config) {
      config.set({
        browsers: ['PhantomJS'],
        files: [
          './test/spec/*.js'
        ],
        frameworks: ['mocha', 'chai', 'sinon'],
        reporters: ['mocha'],
        preprocessors: {
          './test/spec/*.js': ['webpack']
        },
        webpack: {
          module: {
            rules: [
              { test: /\.js/, exclude: /node_modules/, loader: 'babel-loader' }
            ]
          },
          watch: true,
          mode: 'none'
        },
        webpackServer: {
          noInfo: true
        },
        singleRun: true
      });
    };
    

    Finally, I added "test": "karma start karma.config.js" to scripts in package.json. All the spec tests can now be run with npm test.

    0 讨论(0)
提交回复
热议问题