【问题标题】:How to test an ES6 class that needs jquery?如何测试需要 jquery 的 ES6 类?
【发布时间】:2017-11-07 22:06:07
【问题描述】:

我有一个需要 jquery 的 ES6 模块。

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

在我的 main 模块中使用该模块时,它可以正常工作,但问题在于我正在为它编写的测试。

这是测试:

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

这是我的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": "*"
  }
}

如您所见,我只需执行npm test 即可运行测试。

npm test 什么时候出现这个错误:

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

但是我在模块中导入了jquery,为什么会发生这种情况?

【问题讨论】:

    标签: javascript ecmascript-6 mocha.js sinon


    【解决方案1】:

    除非它是具有 default 导出的 ES6 模块正在导入(jQuery 不是),否则导入它的正确方法是

    import * as $ from 'jquery';
    

    import 处理 CommonJS 模块的方式是构建工具的职责。 default 至少在较旧的 Webpack 版本中(错误地)支持非 ES6 模块的导入。并且没有版本限制

    "webpack": "*"
    

    依赖是破坏构建的直接方式。

    【讨论】:

    • 我仍然收到TypeError: $.ajax is not a function。 :(
    • 你究竟是如何测试的?如果就像它所说的那样,"test": "mocha --compilers js:babel-core/register ./test/*.js",那么这将解释很多,显然你不能逃脱。
    • mocha runner 在 Node 中运行,而不是在浏览器中。 Node 中没有 jQuery 的 DOM。 jQuery 是 Node 中的工厂函数 bugs.jquery.com/ticket/14549 ,它确实没有 ajax 属性。如果你愿意在 Node 中运行测试,一个合适的策略是 stub/mock 所有 jQuery 的东西,最好改为使用 $ Angular 提供程序,这将提高可测试性。
    • 这是不正确的。将module.exports 对象视为默认导出是建议的方法。期望 * as $ 对象是 module.exports 是 Babel 和 Webpack 所做的事情,但这不是 Node 会做的事情,也不是你应该依赖的事情。
    • @loganfsmyth 据我所知,这是一种从未标准化的临时行为,对我来说更像是一个错误而不是一个特性。有export.default 的CJS 模块呢?这当然不会增加清晰度。无论如何,导入可能不是这里的主要问题。
    【解决方案2】:

    在我的项目中,我存根了我使用的所有 jQuery 方法,因此,我将存根 $.ajax,因为您需要测试的只是返回的 Promise。

    在另一个文件中是这样的:

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

    那么,在你的测试中

    import { ajax } from 'stubs'
    

    这样,ajax 方法被存根,您可以定义它应该在每个测试的基础上返回什么:

    ajax.returns({
      done: (callback) => { callback(someParam) },
      fail: () => {},
      always: (callback) => { callback() }
    });
    

    【讨论】:

    • 这对 OP 没有帮助。 jQuery 绑定在要测试的代码中,因此发布者要么需要拦截 require 调用,要么将 jQuery 注入模块中。只有这样,您的代码才会有价值。
    【解决方案3】:

    这里有两个主要问题。第一个当然是您需要解决导入问题,但这与测试无关。您需要在进行测试之前解决此问题,这可能与构建工具的配置与在 Node.js 中运行有关。您应该为此打开一个单独的问题,although this might be of help。可能您需要做的就是将导入替换为 import * as jQuery from 'jquery';

    另一个大问题是您在 Node 内运行它(使用触发 Mocha 的 npm test)而 您的代码需要浏览器。 Sinon 的假服务器实现旨在用于浏览器环境,而您正在服务器环境中运行测试。这意味着 jQuery 和假服务器设置都不起作用,因为 Node 没有XHR object

    因此,尽管 Sinon XHR 设置看起来不错,除非您愿意更改测试运行器以在浏览器环境中运行测试(Karma 非常适合从 CLI 执行此操作!),您需要处理此问题用另一种方式。我很少去伪造 XHR,而是在更高级别上删除依赖项。 @CarlMarkham 的回答涉及到这一点,但他没有详细说明这将如何与您的代码一起使用。

    在 Node 中运行代码时,您基本上有两个选择:

    1. 拦截导入 JQuery 模块的调用,并将其替换为您自己的对象,该对象的存根版本为 ajax。这需要一个模块加载器拦截器,例如 rewireproxyquire
    2. 直接在您的模块中使用依赖注入。

    Sinon主页在第一个选项上有a good article by Morgan Roderick,还有几个links to other articles elsewhere on the net,但没有说明如何做第一个选项。有空的时候我应该写一篇……但这里是:

    在实例级别使用依赖注入

    侵入性最小的方法是在您正在测试的实例上公开ajax 方法。这意味着您不需要向模块本身注入任何东西,并且您不必考虑事后清理:

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

    我已经写了a more elaborate example of this technique on the Sinon issuetracker。

    还有另一种方式,更具侵入性,但可以通过直接修改模块的依赖关系来保持类代码不变:

    在模块级别使用依赖注入

    只需修改您的 Weather 类,为您的依赖项导出一个 setter 接口,以便可以覆盖它们:

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

    现在您可以将测试简化为如下所示:

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

    这种方法的一个问题是您正在篡改模块的内部结构,因此您需要恢复 jQuery 对象,以防您需要在其他测试中使用 Wea​​ther 类。你当然也可以反过来:你可以导出 actual jQuery 对象并直接修改 ajax 方法,而不是注入一个假的 jQuery 对象。然后,您将删除上面示例代码中的所有注入代码,并将其修改为类似

    // 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
    

    【讨论】:

    • 因为这些天我经常旅行,所以我没有时间好好看看这个,但我认为这或多或少是我在寻找的答案。快速提问,什么是开始现代 Web 应用程序开发的好构建工具?网页包?
    • Webpack 很好,但设置总是一件苦差事。尝试create-react-app 获取整个shebang 设置。
    【解决方案4】:

    我最近遇到了类似的问题。我发现当我使用 mocha 和 webpack 运行测试时,jquery 绑定的范围内没有“窗口”,因此它是未定义的。为了解决这个问题,我发现可以按照this advice,将源文件中的import * as $ from jquery替换为:

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

    但是源文件将不再与 webpack 正确捆绑,所以我放弃了使用 mocha 和 babel 进行客户端 javascript 测试。相反,我发现我可以使用 karma 和 phantomjs 的组合正确测试我的客户端代码。

    首先,我安装了所有依赖项:

    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
    

    然后我在根目录中设置了一个名为 karma.config.js 的配置文件:

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

    最后,我在 package.json 中的脚本中添加了"test": "karma start karma.config.js"。现在可以使用npm test 运行所有规范测试。

    【讨论】:

      猜你喜欢
      • 2016-07-12
      • 1970-01-01
      • 1970-01-01
      • 2018-09-20
      • 2021-12-06
      • 1970-01-01
      • 2014-07-02
      • 1970-01-01
      • 2016-12-21
      相关资源
      最近更新 更多