如何模拟 ES6 模块的导入?

我有以下ES6模块:

文件网络.js

export function getDataFromServer() {
  return ...
}

文件小部件.js

import { getDataFromServer } from 'network.js';

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

  render() {
    ...
  }
}

我正在寻找一种方法来测试带有模拟实例的 Widget。如果我使用单独的s而不是ES6模块,就像在Karma中一样,我可以像这样编写我的测试:getDataFromServer<script>

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

好吧,但是现在不可用(好吧,根本没有),我不知道如何将东西直接注入自己的范围。getDataFromServerwindowwindowwidget.js

那么我该何去何从呢?

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

我考虑过的东西:

一个。手动依赖注入。

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

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

我对像这样搞砸Widget的公共界面并公开实现细节感到非常不舒服。不行。


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

像这样:

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

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


答案 1

我已经开始在我的测试中使用这种风格,它将模块中的所有导出作为对象的属性导入,然后可以对其进行模拟。我发现这比使用rewire或proxyquire或任何类似技术要干净得多。例如,在需要模拟 Redux 操作时,我最常这样做。以下是我在上面的示例中可能使用的内容:import * as obj

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

如果你的函数碰巧是默认导出,那么将产生,你可以模拟network.default。import * as network from './network'{default: getDataFromServer}

注意:ES规范将模块定义为只读,许多ES转译器已经开始遵守这一点,这可能会破坏这种间谍风格。这高度依赖于您的转译器以及测试框架。例如,我认为Jest表演了一些魔法来使这项工作发挥作用,尽管Jasmine没有,至少目前是这样。新浪网.


答案 2

carpeliam 是正确的,但请注意,如果要监视模块中的函数并使用该模块中的另一个函数调用该函数,则需要将该函数作为导出命名空间的一部分调用,否则将不会使用 spy。

错误示例:

// File mymodule.js

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

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

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