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