如何模拟ES6模块的导入?

时间:2016-02-06 11:28:40

标签: javascript unit-testing mocha ecmascript-6

我有以下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>而不是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();
  });
});

好的,但现在getDataFromServer window无法使用window(好吧,根本没有widget.js),我也不知道如何通过将内容直接注入widget.js自己的范围。

那么我从哪里开始?

  1. 有没有办法访问Widget的范围,或至少用我自己的代码替换其导入?
  2. 如果没有,我该如何让widget.js可测试?
  3. 我考虑的东西:

    一个。手动依赖注入。

    export class Widget() { constructor(deps) { deps.getDataFromServer("dataForWidget") .then(data => this.render(data)); } } 删除所有导入,并期望调用者提供deps。

    import { getDataFromServer } from 'network.js';
    
    export let deps = {
      getDataFromServer
    };
    
    export class Widget() {
      constructor() {
        deps.getDataFromServer("dataForWidget")
        .then(data => this.render(data));
      }
    }
    

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

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

    类似的东西:

    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而不是angular.module('app', ['ngComponentRouter', 'dialog', 'heroes', 'crisis-center']) .config(function($locationProvider) { $locationProvider.html5Mode(true); }) .run(function($router) { $router.config([ { path: '/...', name: 'App', component: 'app', useAsDefault: true } ]); $router.navigate(['App']); }) .component('app', { template: '<nav>\n' + ' <a ng-link="[\'CrisisCenter\']">Crisis Center</a>\n' + ' <a ng-link="[\'Heroes\']">Heroes</a>\n' + '</nav>\n' + '<ng-outlet></ng-outlet>\n', $routeConfig: [ {path: '/crisis-center/...', name: 'CrisisCenter', component: 'crisisCenter', useAsDefault: true}, {path: '/heroes/...', name: 'Heroes', component: 'heroes'}, {path: '/disaster', name: 'Asteroid', redirectTo: ['CrisisCenter', 'CrisisDetail', {id:3}]} ] }); 的风险。我对此感到不安,但到目前为止,这是我最好的主意。

9 个答案:

答案 0 :(得分:115)

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

答案 1 :(得分:28)

@carpeliam是正确的但请注意,如果你想窥探模块中的一个函数并在该模块中调用该函数使用另一个函数,你需要将该函数作为exports命名空间的一部分调用,否则间谍不会使用。

错误的例子:

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

答案 2 :(得分:7)

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

答案 3 :(得分:4)

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

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

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

答案 4 :(得分: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);
  });
});

请参阅the doc

答案 5 :(得分:1)

我最近发现babel-plugin-mockable-imports可以很好地处理此问题,恕我直言。如果您已经在使用Babel,那么值得研究。

答案 6 :(得分:0)

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

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一起使用,但尽管如此,它是一种模拟模块的精巧解决方案,否则很难模拟。

答案 7 :(得分:0)

请参见假设我想模拟从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();
}

答案 8 :(得分:0)

您可以为此使用基于 putout 的库 mock-import

假设您有一个要测试的代码,让它成为cat.js

import {readFile} from 'fs/promises';

export default function cat() {
    const readme = await readFile('./README.md', 'utf8');
    return readme;
};

使用名称 test.jstap-based 测试看起来像这样:

import {test, stub} from 'supertape';
import {createImport} from 'mock-import';

const {mockImport, reImport, stopAll} = createMockImport(import.meta.url);

// check that stub called
test('cat: should call readFile', async (t) => {
    const readFile = stub();
    
    mockImport('fs/promises', {
        readFile,
    });
    
    const cat = await reImport('./cat.js');
    await cat();
    
    stopAll();
    
    t.calledWith(readFile, ['./README.md', 'utf8']);
    t.end();
});

// mock result of a stub
test('cat: should return readFile result', async (t) => {
    const readFile = stub().returns('hello');
    
    mockImport('fs/promises', {
        readFile,
    });
    
    const cat = await reImport('./cat.js');
    const result = await cat();
    
    stopAll();
    
    t.equal(result, 'hello');
    t.end();
});

要运行测试,我们应该添加 --loader 参数:

node --loader mock-import test.js

或者使用NODE_OPTIONS

NODE_OPTIONS="--loader mock-import" node test.js

在底层 mock-import 使用 transformSource 钩子,它将所有 imports 即时替换为这样的形式的常量声明:

const {readFile} = global.__mockImportCache.get('fs/promises');

因此 mockImportMap 中添加新条目,stopAll 清除所有模拟,因此测试不会重叠。

需要所有这些东西,因为 ESM has it's own separate cache 和用户空间代码无法直接访问它。