Как издеваться над импортом модуля ES6?

У меня есть следующие модули 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 с помощью mock-экземпляра getDataFromServer. Если бы я использовал отдельные <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 или, по крайней мере, заменить его импорт моим собственным кодом?
  • Если нет, как я могу сделать Widget testable?

Материал, который я рассмотрел:

а. Ручная инъекция зависимостей.

Удалите все импорт из widget.js и ожидайте, что вызывающий абонент предоставит отпечатки.

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

Это менее инвазивно, но требует, чтобы я написал много шаблонов для каждого модуля, и по-прежнему существует риск, что я все время использую getDataFromServer вместо deps.getDataFromServer. Я беспокоюсь об этом, но это моя лучшая идея.

Ответ 1

Я начал использовать стиль 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}, и вы можете mock network.default.

Ответ 2

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

Ответ 3

Ответ @vdloo направил меня в правильном направлении, но использование обоих ключевых слов commonjs "export" и модуля ES6 "export" вместе в одном файле мне не помогло (webpack v2 или более поздняя версия жалуется). Вместо этого я использую экспорт по умолчанию (именованная переменная), оборачивая все экспорты отдельных именованных модулей, а затем импортируя экспорт по умолчанию в мой файл тестов. Я использую следующую настройку экспорта с mocha/sinon, и работа с заглушками работает нормально, не требуя перепрограммирования и т.д.:

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

Ответ 4

Я реализовал библиотеку, которая пытается решить проблему имитации импорта классов Typescript во время выполнения, не требуя, чтобы исходный класс знал о каком-либо явном внедрении зависимостей.

Библиотека использует import * as синтаксиса, а затем заменяет исходный экспортированный объект классом-заглушкой. Он сохраняет безопасность типов, поэтому ваши тесты будут ломаться во время компиляции, если имя метода было обновлено без обновления соответствующего теста.

Эту библиотеку можно найти здесь: ts-mock-import.

Ответ 5

Я обнаружил, что этот синтаксис работает:

Мой модуль:

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

Смотрите док.

Ответ 6

Я сам не пробовал, но думаю, издевательство может сработать. Это позволяет вам заменить реальный модуль на предоставленный вами макет. Ниже приведен пример, чтобы дать вам представление о том, как это работает:

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, но, тем не менее, это отличное решение для насмешливых модулей, которые в противном случае трудно смоделировать.