如何模拟 ES6 module的导入?

IT技术 javascript unit-testing mocha.js ecmascript-6
2021-02-27 00:42:16

我有以下 ES6 module:

文件network.js

export function getDataFromServer() {
  return ...
}

文件小部件.js

import { getDataFromServer } from 'network.js';

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

  render() {
    ...
  }
}

我正在寻找一种方法来测试 Widget 的模拟实例getDataFromServer如果我使用单独的<script>s 而不是 ES6 module,就像在 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 module(例如使用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并期望调用者提供 deps。

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

我对像这样弄乱 Widget 的公共接口并暴露实现细节感到非常不舒服。不行。


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

就像是:

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

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

6个回答

我已经开始import * as obj在我的测试中使用这种样式,它将module中的所有导出作为对象的属性导入,然后可以对其进行模拟。我发现这比使用 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。

注意:ES 规范将module定义为只读,许多 ES 转译器已经开始尊重这一点,这可能会打破这种间谍风格。这在很大程度上取决于您的转译器以及您的测试框架。例如,我认为 Jest 使用了一些魔法来完成这项工作,尽管Jasmine 没有,至少目前没有天啊。

Jasmine 抱怨[method_name] is not declared writable or has no setter这是有道理的,因为 es6 导入是恒定的。有没有办法解决?
2021-05-01 00:42:16
您是import * as obj在测试中还是在常规代码中使用only ?
2021-05-09 00:42:16
@carpeliam 这不适用于导入为只读的 ES6 module规范。
2021-05-11 00:42:16
@Francisc import(不像require,可以去任何地方)被提升,所以你不能在技术上多次导入。听起来你的间谍在别处被召唤?为了防止测试混乱状态(称为测试污染),您可以在 afterEach 中重置您的间谍(例如 sinon.sandbox)。我相信茉莉花会自动做到这一点。
2021-05-11 00:42:16
@agent47 问题是,虽然 ES6 规范专门阻止了这个答案的工作,但正如你提到的那样,大多数import用 JS编写的人并没有真正使用 ES6 module。像 webpack 或 babel 这样的东西会在构建时介入,并将其转换成它们自己的内部机制来调用代码的远处部分(例如__webpack_require__),或者转换成 ES6 之前的事实标准之一,CommonJS、AMD 或 UMD。而且这种转换通常不严格遵守规范。所以对于很多很多开发人员来说,这个答案很好用。目前。
2021-05-15 00:42:16

carpeliam 是正确的,但请注意,如果您想监视module中的一个函数并在该module中使用另一个调用该函数的函数,则需要将该函数作为exports 命名空间的一部分来调用,否则将不会使用该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
    });
});
请注意,这种使用export functionwith 的建议在exports.myfunc2技术上混合了 commonjs 和 ES6 module语法,这在需要全有或全无 ES6 module语法使用的较新版本的 webpack (2+) 中是不允许的。我在下面添加了一个基于这个的答案,它将在 ES6 严格的环境中工作。
2021-04-17 00:42:16
我希望我能再投票这个答案 20 次!谢谢!
2021-04-21 00:42:16
@ColinWhitmarshexports.myfunc2是对myfunc2直到spyOn将其替换为对间谍函数的引用的直接引用spyOn将更改 的值exports.myfunc2并将其替换为 spy 对象,而myfunc2在module的范围内保持不变(因为spyOn无法访问它)
2021-04-30 00:42:16
不应该*冻结对象导入并且对象属性不能更改吗?
2021-05-02 00:42:16
有人可以解释为什么会这样吗?export.myfunc2() 是 myfunc2() 的副本而不是直接引用吗?
2021-05-09 00:42:16

vdloo 的回答让我朝着正确的方向前进,但是在同一个文件中同时使用CommonJS “exports”和 ES6 module“export”关键字对我不起作用(Webpack v2 或更高版本抱怨)。

相反,我使用默认(命名变量)导出来包装所有单独的命名module导出,然后在我的测试文件中导入默认导出。我将以下导出设置与Mocha /Sinon一起使用,并且 stubbing 工作正常而无需rewire等:

// 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);
  });
});
这似乎是对另一个问题的回答!widget.js 和 network.js 在哪里?这个答案似乎没有传递依赖,这就是使原始问题变得困难的原因。
2021-04-18 00:42:16
@ConspicuousCompiler 感谢您的提醒 - 这是一个错误,我不打算用我的工作电子邮件链接 SO 帐户修改此答案。
2021-04-23 00:42:16
@QuarkleMotion:看来您无意中使用与主帐户不同的帐户编辑了此内容。这就是为什么您的编辑必须经过手动批准 - 它看起来不像是您的 我认为这只是一个意外,但是,如果是故意的,您应该阅读有关 sock puppet 帐户的官方政策,以便您不要不小心违反规则
2021-04-24 00:42:16
有用的答案,谢谢。只是想提一下,let MyModule不需要使用默认导出(它可以是原始对象)。此外,此方法不需要myfunc1()调用myfunc2(),它可以直接监视它。
2021-05-09 00:42:16

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

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

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

这个module需要更多的github star
2021-05-13 00:42:16

我发现此语法有效:

我的module:

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

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

我的module的测试代码:

// File 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 和一些额外的说明:似乎只适用于节点module,即你在 package.json 上的东西。更重要的是,Jest 文档中没有提到的东西,传递给的字符串jest.mock()必须匹配 import/packge.json 中使用的名称而不是常量的名称。在文档中,它们都是相同的,但是使用类似import jwt from 'jsonwebtoken'您需要将模拟设置为jest.mock('jsonwebtoken')
2021-05-16 00:42:16