如何模拟ES6模块的导入?


141

我有以下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() {
    ...
  }
}

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

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

好的,但是现在getDataFromServer不可用window(嗯,根本没有window),而且我不知道一种将东西直接注入widget.js自己的作用域的方法。

那我从这里去哪里呢?

  1. 有没有办法访问的范围widget.js,或者至少用我自己的代码替换其导入?
  2. 如果没有,我该如何进行Widget测试?

我考虑过的东西:

一个。手动依赖项注入。

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

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

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


如果没有这种导入的本模拟支持,我可能会考虑编写自己的转换器,以便将您的ES6样式的导入转换为自定义的可模拟导入系统。这肯定会增加另一层可能的故障,并更改您要测试的代码,...。
t.niese,2016年

我现在无法设置测试套件,但是我会尝试使用jasmincreateSpygithub.com/jasmine/jasmine/blob/…)函数,并从'network.js'模块导入对getDataFromServer的引用。这样,在小部件的测试文件中,您将导入getDataFromServer,然后将let spy = createSpy('getDataFromServer', getDataFromServer)
Microfed 2016年

第二个猜测是从“ network.js”模块返回一个对象,而不是一个函数。这样,您就可以spyOnnetwork.js模块导入该对象。它始终是对同一对象的引用。
Microfed '16

实际上,从我所看到的来看,它已经是一个对象:babeljs.io/repl/…–
Microfed

2
我真的不明白依赖注入如何弄乱Widget公共接口?Widget被搞砸了没有 deps。为什么不明确显示依赖性?
thebearingedge

Answers:


129

我已经开始import * as obj在测试中使用该样式,该样式将从模块中导入所有导出内容,然后将其作为对象的属性进行模拟。我发现这比使用诸如rewire或proxyquire或任何类似技术之类的方法干净得多。例如,在需要模拟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。


3
import * as obj在测试中还是在常规代码中都使用了?
洲泰

36
@carpeliam这不适用于导入为只读的ES6模块规范。
ashish

7
茉莉花在抱怨[method_name] is not declared writable or has no setter这是有道理的,因为es6的导入是恒定的。有没有解决方法?
lpan

2
@Francisc import(不同于require,它可以到任何地方)都被吊起,因此从技术上讲您不能多次导入。听起来您的间谍正在其他地方被叫?为了防止测试陷入混乱状态(称为测试污染),可以在afterEach(例如sinon.sandbox)中重置间谍。我相信茉莉花会自动这样做。
carpeliam

10
@ agent47问题是,虽然ES6规范import确实按照您提到的方式专门阻止了此答案,但是大多数用JS 编写的人并没有真正使用ES6模块。诸如webpack或babel之类的东西将在构建时介入,并将其转换为自己的内部机制以调用代码的较远部分(例如__webpack_require__),或转换为ES6之前的事实上的标准之一,即CommonJS,AMD或UMD。而且这种转换通常并不严格遵守规范。因此,对于许多开发人员来说,这个答案很好。目前。
daemonexmachina

31

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

错误的例子:

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

4
我希望我能再投票20次以上!谢谢!
sfletche'4

有人可以解释为什么会这样吗?Exports.myfunc2()是否是myfunc2()的副本而没有直接引用?
科林·惠特玛什

2
@ColinWhitmarsh exports.myfunc2是直接引用,myfunc2直到spyOn将其替换为对间谍函数的引用。spyOn将更改的值exports.myfunc2并将其替换为间谍对象,而myfunc2在模块范围内保持不变(因为spyOn无法访问它)
madprog

不应*冻结导入对象并且不能更改对象属性?
agent47

1
请注意,此推荐export function与之同时使用exports.myfunc2是在技术上混合了commonjs和ES6模块语法,并且在要求全部使用或不使用ES6模块语法的较新版本的webpack(2+)中,不允许这样做。我在此基础上添加了一个答案,该答案将在ES6严格环境中工作。
QuarkleMotion '18

6

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

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

可以在以下位置找到该库:ts-mock-imports


1
该模块需要更多的github星星
-SD,

6

@vdloo的答案使我朝着正确的方向前进,但是在同一个文件中同时使用commonjs“ exports”和ES6 module“ export”关键字对我来说不起作用(webpack v2或更高版本抱怨)。取而代之的是,我使用默认的(命名变量)导出包装所有单独的命名模块导出,然后将默认导出导入我的测试文件中。我正在将以下导出设置与mocha / sinon一起使用,并且无需重新布线等即可进行存根(stub),可以正常工作:

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

有用的答案,谢谢。只是想提一下,let MyModule不需要使用默认导出(它可以是原始对象)。另外,此方法不需要myfunc1()调用myfunc2(),它可以直接监视它。
马克·爱丁顿

@QuarkleMotion:似乎是您偶然使用与主帐户不同的帐户编辑了此帐户。这就是为什么您的编辑必须经过人工批准的原因-看起来好像不是您的, 我认为这只是偶然,但是,如果是有意的,则应阅读有关袜子木偶帐户的官方政策,以便您不要偶然违反规则
出色的编译器,

1
@ConspicuousCompiler感谢您的注意-这是一个错误,我无意使用与我的工作相关的SO帐户修改此答案。
QuarkleMotion,

这似乎是对另一个问题的答案!widget.js和network.js在哪里?这个答案似乎没有传递依赖性,这使原来的问题变得很难。
Bennett McElwee

3

我发现此语法有效:

我的模块:

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

请参阅文件


+1和一些附加说明:似乎仅适用于节点模块,即package.json上的内容。更重要的是,Jest文档中未提及的内容,传递给的字符串jest.mock()必须与import / packge.json中使用的名称匹配,而不是常量的名称。在文档中它们都是相同的,但是使用类似代码的代码时,import jwt from 'jsonwebtoken'您需要将模拟程序设置为jest.mock('jsonwebtoken')
kaskelotti

0

我自己还没有尝试过,但是我认为嘲笑可能有用。它允许您用提供的模拟代替实际模块。以下是一个示例,可让您了解其工作原理:

mockery.enable();
var networkMock = {
    getDataFromServer: function () { /* your mock code */ }
};
mockery.registerMock('network.js', networkMock);

import { Widget } from 'widget.js';
// This widget will have imported the `networkMock` instead of the real 'network.js'

mockery.deregisterMock('network.js');
mockery.disable();

似乎mockery不再需要维护了,我认为它仅可与Node.js一起使用,但无论如何,它是一种精巧的解决方案,用于模拟难以模拟的模块。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.