Javascript 如何模拟ES6模块的导入?
我有以下ES6模块: 文件network.js 文件widget.js 我正在寻找一种使用模拟实例Javascript 如何模拟ES6模块的导入?,javascript,unit-testing,mocha.js,ecmascript-6,Javascript,Unit Testing,Mocha.js,Ecmascript 6,我有以下ES6模块: 文件network.js 文件widget.js 我正在寻找一种使用模拟实例getDataFromServer测试小部件的方法。如果我使用单独的而不是ES6模块,比如在Karma中,我可以编写如下测试: describe("widget", function() { it("should do stuff", function() { let getDataFromServer = spyOn(window, "
getDataFromServer
测试小部件的方法。如果我使用单独的
而不是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模块(如+),我会编写如下内容:
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();
});
});
import { getDataFromServer } from 'network.js';
export let deps = {
getDataFromServer
};
export class Widget() {
constructor() {
deps.getDataFromServer("dataForWidget")
.then(data => this.render(data));
}
}
好的,但是现在getDataFromServer
在window
中不可用(好吧,根本没有window
),而且我不知道如何将内容直接注入widget.js
自己的范围
那我该怎么办?
widget.js的范围,或者至少用我自己的代码替换它的导入?
小部件
可测试?我考虑过的东西: A.手动依赖注入。 从
widget.js
中删除所有导入,并期望调用者提供DEP
export class Widget() {
constructor(deps) {
deps.getDataFromServer("dataForWidget")
.then(data => this.render(data));
}
}
我对像这样弄乱Widget的公共界面并公开实现细节感到非常不舒服。不行
B公开导入以允许模拟它们。 比如:
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();
});
});
import { getDataFromServer } from 'network.js';
export let deps = {
getDataFromServer
};
export class Widget() {
constructor() {
deps.getDataFromServer("dataForWidget")
.then(data => this.render(data));
}
}
然后:
这是一种侵入性较小的方法,但它需要我为每个模块编写大量的样板文件,而且我始终使用
getDataFromServer
而不是deps.getDataFromServer
仍然存在风险。我对此感到不安,但这是我迄今为止最好的想法。我已经开始在测试中使用import*作为obj
样式,它将模块中的所有导出作为对象的属性导入,然后可以对其进行模拟。我发现这比使用诸如重新布线、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.,但是请注意,如果您想监视模块中的函数并使用该模块中调用该函数的另一个函数,您需要将该函数作为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
});
});
我发现这种语法有效: 我的模块:
// File mymod.js
import shortid from 'shortid';
const myfunc = () => shortid();
export default myfunc;
我的模块的测试代码:
// 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);
});
});
请参阅。使我朝着正确的方向前进,但在同一文件中同时使用“exports”和ES6模块“export”关键字对我不起作用(v2或更高版本的投诉)
相反,我使用一个默认(命名变量)导出来包装所有单独的命名模块导出,然后在测试文件中导入默认导出。我正在使用/Sinon和stubing的以下导出设置,无需重新布线即可正常工作:
我实现了一个库,它试图解决TypeScript类导入的运行时模拟问题,而不需要原始类知道任何显式依赖项注入 库使用
import*作为
语法,然后用存根类替换原始导出的对象。它保留了类型安全性,因此,如果方法名称已更新而未更新相应的测试,则测试将在编译时中断
这个库可以在这里找到:。我自己没有试过,但我认为可能有用。它允许您用您提供的模拟来替换实际模块。下面是一个让您了解其工作原理的示例:
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();
似乎不再维护
mockry
,我认为它只适用于Node.js,但尽管如此,它还是一个用于模拟其他难以模拟的模块的简洁解决方案。我最近发现了一个可以巧妙处理此问题的解决方案,IMHO。如果您已经在使用,则值得研究。请参阅假设我想模拟从isDevMode()
函数返回的结果,以便检查代码在特定情况下的行为
下面的示例针对以下设置进行测试
"@angular/core": "~9.1.3",
"karma": "~5.1.0",
"karma-jasmine": "~3.3.1",
下面是一个简单测试用例场景的示例
import * as coreLobrary from '@angular/core';
import { urlBuilder } from '@app/util';
const isDevMode = jasmine.createSpy().and.returnValue(true);
Object.defineProperty(coreLibrary, 'isDevMode', {
value: isDevMode
});
describe('url builder', () => {
it('should build url for prod', () => {
isDevMode.and.returnValue(false);
expect(urlBuilder.build('/api/users').toBe('https://api.acme.enterprise.com/users');
});
it('should build url for dev', () => {
isDevMode.and.returnValue(true);
expect(urlBuilder.build('/api/users').toBe('localhost:3000/api/users');
});
});
src/app/util/url builder.ts的示例内容
import { isDevMode } from '@angular/core';
import { environment } from '@root/environments';
export function urlBuilder(urlPath: string): string {
const base = isDevMode() ? environment.API_PROD_URI ? environment.API_LOCAL_URI;
return new URL(urlPath, base).toJSON();
}
如果这种导入没有本地模拟支持,我可能会考虑为babel编写一个自己的转换器,将ES6风格的导入转换为自定义的可模拟导入系统。这无疑会增加另一层可能的故障,并更改您要测试的代码。我现在无法设置测试套件,但我会尝试使用jasmin的createSpy
()函数,并从“network.js”模块导入对getDataFromServer的引用。因此,在小部件的测试文件中,您将导入getDataFromServer,然后让spy=createSpy('getDataFromServer',getDataFromServer)
第二个猜测是从'network.js'模块返回一个对象,而不是函数。这样,您就可以从network.js
模块导入该对象上的spyOn
。它总是对同一个对象的引用。事实上,它已经是一个对象了,从我所看到的:我真的不明白依赖注入是如何搞乱了小部件的公共接口的<没有deps
,code>Widget
就乱七八糟了。为什么不明确依赖关系?您是仅在测试中还是在常规代码中使用import*作为obj
?@carpeliam这不适用于ES6模块规范,其中导入是只读的。Jasmine正在抱怨[method\u name]未声明为可写或没有setter
,这是有意义的,因为es6导入是常量。有什么解决办法吗?@Francisc导入
(不像require
,它可以去任何地方)被提升,所以从技术上讲,你不能多次导入。听起来你的间谍是贝
import * as coreLobrary from '@angular/core';
import { urlBuilder } from '@app/util';
const isDevMode = jasmine.createSpy().and.returnValue(true);
Object.defineProperty(coreLibrary, 'isDevMode', {
value: isDevMode
});
describe('url builder', () => {
it('should build url for prod', () => {
isDevMode.and.returnValue(false);
expect(urlBuilder.build('/api/users').toBe('https://api.acme.enterprise.com/users');
});
it('should build url for dev', () => {
isDevMode.and.returnValue(true);
expect(urlBuilder.build('/api/users').toBe('localhost:3000/api/users');
});
});
import { isDevMode } from '@angular/core';
import { environment } from '@root/environments';
export function urlBuilder(urlPath: string): string {
const base = isDevMode() ? environment.API_PROD_URI ? environment.API_LOCAL_URI;
return new URL(urlPath, base).toJSON();
}