如何使用ES6模块为单元测试模拟依赖项

61

我试着使用webpack + traceur将Ecmascript 6模块转译为ES5 CommonJS格式,但是我在进行单元测试时遇到了困难。

我尝试使用Jest + traceur预处理器,但是自动模拟和依赖项名称似乎出现了问题,而且我无法在Jest和node-inspector调试中使用sourceMaps。

是否有更好的框架来进行ES6模块的单元测试?


2
我不了解Traceur,但是6to5与jest兼容良好,并且有一个6to5-jest插件 - James Kyle
谢谢James,我会去看看的。它似乎将6to5转换为更易读的形式,并涵盖了我需要的所有ES6功能,所以也许我会选择这条路。 - Evan Layman
另外,你能否使用node-inspector调试来获取jest的sourceMaps呢? - Evan Layman
1
我还建议使用Babel.js将你的ES6代码转换为ES5。在转换测试或实现代码时,我没有遇到任何问题。https://babeljs.io/ - Carlos Pinto
1
如果您正在使用Babel,那么您也可以使用babel-plugin-rewire来模拟模块,而不需要专门的webpack或browserify加载器。 - Daniel A. R. Werner
正是我所需要的,@DanielA.R.Werner,谢谢。 - MaxPRafferty
5个回答

58

我已经开始在我的测试中使用import * as obj的语法,它会将模块中所有导出作为对象的属性导入,然后可以对其进行模拟。相比使用Rewire或Proxyquire等类似技术,我认为这样做更加简洁。

关于使用traceur框架的问题,我无法发表意见。但是,我在Karma、Jasmine和Babel的环境下实现了这个方法,并且将其发布在这里,因为这似乎是这种问题中最受欢迎的。

当需要模拟Redux actions时,我经常使用这种策略。以下是一个简短的示例:

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

2
这种模式或技术应该有更好的文档记录,因为它是一种有用的方式来模拟ES6模块,而不是依赖第三方库。随着ES6变得越来越流行,我很想看到在单元测试方面,当导入模块时,这种模式在示例中更常见,而不是使用import {someExport} from module符号。 - markyph
既然你提到了redux actions,值得指出的是,必须密切注意何时将actions绑定到连接的组件上。对已经绑定的函数应用spy可能没有效果。理解不同调用react-redux的connect()函数的方式对我来说非常关键,这让我在不失去理智的情况下使其正常工作:https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options - killthrush
2
你如何防止测试相互影响?我的经验是,模拟导入会导致后续测试使用模拟实例而不是真实实例,反之亦然。 - tom
@user831865 根据你的测试框架,模拟的范围应该是定义模拟对象的 describe 块。不过如果不了解你特定的设置,很难确定。 - carpeliam
1
如果你正在使用sinon,sinon.sandbox可以防止你的模拟数据影响到其他测试。 - carpeliam
谢谢 @carpeliam!! 它非常适合我的使用情况! - emanuelbsilva

7
如果您正在使用Webpack,除了rewire之外还有一个选项比较灵活,那就是inject-loader
例如,在与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()
  })
})

与proxyquire类似的是,inject-loader允许在导入之前注入依赖项,而在rewire中必须先导入再重写,这使得对某些组件(例如那些具有某些初始化的组件)进行模拟变得不可能。


是的,但这仅适用于您使用require而不是import .. as ..语法。 - alwe
1
不是真的。我在ES6导入语法中成功使用了它。我想这取决于你的设置,但我使用babel-loader和Babel。它适用于require和ES6导入。唯一需要require的地方是在测试本身中模拟依赖项。 - djskinner
嗨@djskinner。我正在尝试使用babel、webpack和karma来启动inject-loader,但遇到了一些问题。您能否在您的答案中添加您的karma.config.js(如果您使用的话还有webpack.config文件)。提前感谢。 - Jon P Smith
请查看这个答案 作为一个好的起点。它主要是关于代码覆盖率方面,但即使你对代码覆盖率没有兴趣,基本配置也是很不错的选择。至于inject-loader ,我不需要任何额外的配置,只需 npm i inject-loader 然后 require(inject!...) 即可。 - djskinner
如何在使用AMD模块时使用它?我收到了“上下文中尚未加载模块名称“inject!src / qux_unnormalized2”的错误信息”。 - Stefan

5

您可以使用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);
    });
});

3

我实际上通过放弃Jest,改用Karma + Jasmine + Webpack,并使用https://github.com/jhnns/rewire来模拟依赖项,使这一切都能够工作。


1
函数内的变量不能被 rewire 改变。这是由 JavaScript 约束的,无法通过 rewire 规避。 - John Wu
3
我很高兴这条建议对你有用...但是它为什么被接受为答案呢?“不要使用那个”并不一定有帮助 :( - Steve

1

Proxyquire将会帮助您,但它无法与现代的webpack+ES6模块一起使用,即“别名”。

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

行不通。只要您可以模拟fs-您无法模拟reducers。 在任何webpack或babel转换之后,您必须指定依赖项的real名称。通常是相对于module1位置的名称。可能是'../../../shared/core/reducers'。也可能不是。

https://github.com/theKashey/proxyquire-webpack-alias(稳定,基于proxyquire分支)或https://github.com/theKashey/resolveQuire(不太稳定,可以在原始proxyquire上运行)的解决方案。

两者都很有效,并且将以proxyquire方式(这是一个好方法)模拟任何ES6模块(它们非常好)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接