如何模拟ES6模块的导入?

Kos*_*Kos 133 javascript unit-testing mocha.js ecmascript-6

我有以下ES6模块:

network.js

export function getDataFromServer() {
  return ...
}
Run Code Online (Sandbox Code Playgroud)

widget.js

import { getDataFromServer } from 'network.js';

export class Widget() {
  constructor() {
    getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }

  render() {
    ...
  }
}
Run Code Online (Sandbox Code Playgroud)

我正在寻找一种方法来测试Widget的模拟实例getDataFromServer.如果我使用单独的<script>s代替ES6模块,比如在Karma中,我可以编写我的测试:

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(window, "getDataFromServer").andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});
Run Code Online (Sandbox Code Playgroud)

但是,如果我在浏览器之外单独测试ES6模块(比如使用Mocha + babel),我会写一些类似于:

import { Widget } from 'widget.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(?????) // How to mock?
    .andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});
Run Code Online (Sandbox Code Playgroud)

好的,但现在getDataFromServer没有window(好吧,根本没有window),我不知道将东西直接注入widget.js自己的范围的方法.

那么我从哪里开始呢?

  1. 有没有办法widget.js用我自己的代码访问范围,或至少替换它的导入?
  2. 如果没有,我怎样才能Widget测试?

我考虑的东西:

一个.手动依赖注入.

删除所有导入widget.js并期望调用者提供deps.

export class Widget() {
  constructor(deps) {
    deps.getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }
}
Run Code Online (Sandbox Code Playgroud)

我非常不喜欢弄乱Widget这样的公共界面并暴露实现细节.不行.


湾 公开导入以允许模拟它们.

就像是:

import { getDataFromServer } from 'network.js';

export let deps = {
  getDataFromServer
};

export class Widget() {
  constructor() {
    deps.getDataFromServer("dataForWidget")
    .then(data => this.render(data));
  }
}
Run Code Online (Sandbox Code Playgroud)

然后:

import { Widget, deps } from 'widget.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(deps.getDataFromServer)  // !
      .andReturn("mockData");
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});
Run Code Online (Sandbox Code Playgroud)

这种侵入性较小,但要求我为每个模块编写大量样板文件,而且我仍然有使用getDataFromServer而不是deps.getDataFromServer一直使用的风险.我对此感到不安,但到目前为止,这是我最好的主意.

car*_*iam 121

我已经开始import * as obj在我的测试中使用该样式,它将模块中的所有导出作为对象的属性导入,然后可以对其进行模拟.我发现这比使用重新布线或代理或任何类似技术更清洁.例如,当我需要模拟Redux动作时,我经常这样做.以下是我可能用于上述示例的内容:

import * as network from 'network.js';

describe("widget", function() {
  it("should do stuff", function() {
    let getDataFromServer = spyOn(network, "getDataFromServer").andReturn("mockData")
    let widget = new Widget();
    expect(getDataFromServer).toHaveBeenCalledWith("dataForWidget");
    expect(otherStuff).toHaveHappened();
  });
});
Run Code Online (Sandbox Code Playgroud)

如果您的函数恰好是默认导出,那么import * as network from './network'就会生成{default: getDataFromServer}并且您可以模拟network.default.

  • @carpeliam这不适用于ES6模块规范,其中导入是只读的. (29认同)
  • Jasmine抱怨`[method_name]没有声明可写或没有setter`这是有意义的,因为es6导入是常量.有办法解决吗? (6认同)
  • @ agent47问题在于虽然ES6规范专门阻止了这个答案的工作,但正如你提到的那样,大多数在JS中编写`import`的人并没有真正使用ES6模块.像webpack或babel这样的东西会在构建时进入,并将其转换为自己的内部机制,用于调用代码的远端部分(例如`__webpack_require__`)或者转换为ES6*事实上*之前的标准,CommonJS, AMD或UMD.而且转换通常不严格遵守规范.所以对于很多很多开发者来说,这个答案很好.目前. (6认同)
  • 您是仅在测试中还是在常规代码中使用“ import * as obj”? (3认同)
  • @Francisc`import`(不同于`require`,它可以到任何地方)被吊起,因此从技术上讲您不能多次导入。听起来您的间谍正在其他地方被叫?为了防止测试陷入混乱状态(称为测试污染),可以在afterEach(例如sinon.sandbox)中重置间谍。我相信茉莉花会自动这样做。 (2认同)

小智 28

@carpeliam是正确的但请注意,如果你想窥探模块中的一个函数并在该模块中使用另一个函数调用该函数,你需要将该函数作为exports命名空间的一部分调用,否则不会使用spy.

错误的例子:

// mymodule.js

export function myfunc2() {return 2;}
export function myfunc1() {return myfunc2();}

// tests.js
import * as mymodule

describe('tests', () => {
    beforeEach(() => {
        spyOn(mymodule, 'myfunc2').and.returnValue = 3;
    });

    it('calls myfunc2', () => {
        let out = mymodule.myfunc1();
        // out will still be 2
    });
});
Run Code Online (Sandbox Code Playgroud)

正确的例子:

export function myfunc2() {return 2;}
export function myfunc1() {return exports.myfunc2();}

// tests.js
import * as mymodule

describe('tests', () => {
    beforeEach(() => {
        spyOn(mymodule, 'myfunc2').and.returnValue = 3;
    });

    it('calls myfunc2', () => {
        let out = mymodule.myfunc1();
        // out will be 3 which is what you expect
    });
});
Run Code Online (Sandbox Code Playgroud)

  • 我希望我能再多投20个这个答案了!谢谢! (4认同)
  • @ColinWhitmarsh`exports.myfunc2`是对`myfunc2`的直接引用,直到`spyOn`用对间谍函数的引用替换它.`spyOn`将改变`exports.myfunc2`的值并用间谍对象替换它,而`myfunc2`在模块的范围内保持不变(因为'spyOn`无法访问它) (2认同)
  • 请注意,使用“export function”和“exports.myfunc2”的建议在技术上混合了 commonjs 和 ES6 模块语法,这在需要全有或全无 ES6 模块的较新版本的 webpack (2+) 中是不允许的语法用法。我在下面添加了一个基于此答案的答案,该答案将在 ES6 严格环境中工作。 (2认同)

Qua*_*ion 7

@ vdloo的回答让我朝着正确的方向前进,但在同一个文件中同时使用commonjs"exports"和ES6模块"export"关键字对我来说不起作用(webpack v2抱怨).相反,我使用默认(命名变量)导出包装所有单个命名模块导出,然后在我的测试文件中导入默认导出.我正在使用以下导出设置与mocha/sinon和stubing工作正常,无需重新连接等:

// MyModule.js
let MyModule;

export function myfunc2() { return 2; }
export function myfunc1() { return MyModule.myfunc2(); }

export default MyModule = {
  myfunc1,
  myfunc2
}

// tests.js
import MyModule from './MyModule'

describe('MyModule', () => {
  const sandbox = sinon.sandbox.create();
  beforeEach(() => {
    sandbox.stub(MyModule, 'myfunc2').returns(4);
  });
  afterEach(() => {
    sandbox.restore();
  });
  it('myfunc1 is a proxy for myfunc2', () => {
    expect(MyModule.myfunc1()).to.eql(4);
  });
});
Run Code Online (Sandbox Code Playgroud)


Ema*_*ndM 7

我实现了一个库,它试图解决 TypeScript 类导入的运行时模拟问题,而无需原始类知道任何显式依赖注入。

该库使用import * as语法,然后用存根类替换原始导出的对象。它保留了类型安全性,因此如果在没有更新相应测试的情况下更新了方法名称,您的测试将在编译时中断。

这个库可以在这里找到:ts-mock-imports