如何测试需要 jquery 的 ES6 类?

IT技术 javascript ecmascript-6 mocha.js sinon
2021-03-08 21:43:47

我有一个需要 jquery 的 ES6 module。

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在我的mainmodule中使用时工作正常,但问题出在我为它编写的测试中。

这是测试:

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在module中导入了,为什么会发生?

4个回答

这里有两个主要问题。首先当然是您需要修复导入问题,但这与测试无关。在进行测试之前,您需要解决这个问题,这可能与构建工具的配置有关,而不是在 Node.js 中运行。您应该为此打开一个单独的问题,尽管这可能会有所帮助可能你需要做的就是用这个替换导入import * as jQuery from 'jquery';

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

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

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

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

Sinon 主页上有一篇由 Morgan Roderick 撰写的关于第一个选项的好文章,以及指向网络其他地方其他文章的几个链接,但没有说明如何执行第一个选项。我应该在有时间的时候写一篇……但这里是:

在实例级别使用依赖注入

侵入性最小的方法是仅ajax在您正在测试的实例上公开方法。这意味着您不需要向module本身注入任何内容,也不必考虑事后清理:

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

在 Sinon 问题跟踪器了一个更详细的例子来说明这种技术

还有另一种方法,它更具侵入性,但可以让您通过直接修改module的依赖项来保持类代码不变:

在module级别使用依赖注入

只需修改您的 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();
    });
}

这种方法的一个问题是您正在篡改module的内部结构,因此您需要恢复 jQuery 对象,以防您需要在其他测试中使用 Wea​​ther 类。你当然也可以反过来做:你可以导出实际的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 应用程序开发的好构建工具?网络包?
2021-04-22 21:43:47
Webpack 很好,但设置总是一件苦差事。尝试create-react-app获得整个shebang设置。
2021-04-30 21:43:47

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

import * as $ from 'jquery';

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

"webpack": "*"

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

mocharunner 在 Node 中运行,而不是在浏览器中运行。Node.js 中没有 jQuery 的 DOM。jQuery 是 Node 中的工厂函数,bugs.jquery.com/ticket/14549,它确实没有ajax属性。如果您愿意在 Node 中运行测试,正确的策略是存根/模拟所有 jQuery 内容,最好改为使用$Angular 提供程序,这将提高可测试性。
2021-04-23 21:43:47
我仍然得到TypeError: $.ajax is not a function:(
2021-04-29 21:43:47
@loganfsmyth 据我所知,这是从未标准化的临时行为,对我来说更像是一个错误而不是一个功能。那些有的 CJS moduleexport.default呢?这当然不会增加清晰度。无论如何,导入在这里可能不是主要问题。
2021-05-03 21:43:47
这是不正确的。module.exports建议的方法是对象视为默认导出。期望* as $对象module.exports是 Babel 和 Webpack 会做的事情,但这不是 Node 会做的,也不是你应该依赖的事情。
2021-05-09 21:43:47
你具体是怎么测试的?如果就像它所说的那样,"test": "mocha --compilers js:babel-core/register ./test/*.js"那么这将解释很多,显然你无法逃脱。
2021-05-13 21:43:47

在我的项目中,我存根了我使用的所有 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 注入module。只有这样,您的代码才有value。
2021-05-09 21:43:47

我最近遇到了类似的问题。我发现当我使用 mocha 和 webpack 运行测试时,jquery 的范围内没有“窗口”可以绑定到,因此它是未定义的。为了解决这个问题,我发现我可以按照这个建议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
  });
};

最后,我"test": "karma start karma.config.js"在 package.json 中添加了脚本。现在可以使用npm test.