如何模拟ES6模块的导入?

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何模拟ES6模块的导入?相关的知识,希望对你有一定的参考价值。

我有以下ES6模块:

network.js

export function getDataFromServer() {
  return ...
}

widget.js

import { getDataFromServer } from 'network.js';

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

  render() {
    ...
  }
}

我正在寻找一种用getDataFromServer的模拟实例来测试Widget的方法。如果我使用单独的<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();
  });
});

但是,如果我在浏览器之外单独测试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();
  });
});

好吧,但是现在getDataFromServerwindow中没有(好吧,根本就没有window),而且我不知道如何将东西直接注入widget.js自己的范围。

So where do I go from here?

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

我考虑的东西:

a. Manual dependency injection.

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

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

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


b. Expose the imports to allow mocking them.

就像是:

import { getDataFromServer } from 'network.js';

export let deps = {
  getDataFromServer
};

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

然后:

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

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

答案

我已经开始在我的测试中使用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();
  });
});

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

另一答案

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

正确的例子:

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
    });
});
另一答案

@ 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);
  });
});
另一答案

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

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

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

另一答案

我发现这个语法有效:

我的模块:

// mymod.js
import shortid from 'shortid';

const myfunc = () => shortid();
export default myfunc;

我模块的测试代码:

// mymod.test.js
import myfunc from './mymod';
import shortid from 'shortid';

jest.mock('shortid');

describe('mocks shortid', () => {
  it('works', () => {
    shortid.mockImplementation(() => 1);
    expect(myfunc()).toEqual(1);
  });
});

the doc

以上是关于如何模拟ES6模块的导入?的主要内容,如果未能解决你的问题,请参考以下文章

它是模拟ES6模块

如何有条件地导入 ES6 模块?

jest.mock():如何使用工厂参数模拟 ES6 类默认导入

如何使用 ES6 模块导入文件,并将其内容作为字符串读取?

如何有条件地导入ES6模块?

如何将 ES6 导入与“请求”npm 模块一起使用