У меня есть следующие модули 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
. Если бы я использовал отдельные <script>
s вместо модулей 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
тестируемым?
Я рассмотрел:
а. Внедрение зависимостей вручную.
Удалите весь импорт из 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();
});
});
Это менее инвазивно, но требует от меня написания большого количества шаблонов для каждого модуля, и все же существует риск того, что я буду использовать getDataFromServer
вместо него deps.getDataFromServer
все время. Меня это беспокоит, но пока это моя лучшая идея.
createSpy
( github.com/jasmine/jasmine/blob/… ) с импортированной ссылкой на getDataFromServer из модуля 'network.js'. Итак, в файл тестов виджета вы должны импортировать getDataFromServer, а затемlet spy = createSpy('getDataFromServer', getDataFromServer)
spyOn
этот объект, импортированный изnetwork.js
модуля. Это всегда ссылка на один и тот же объект.Widget
публичный интерфейс?Widget
напортачил безdeps
. Почему бы не сделать зависимость явной?Ответы:
Я начал использовать этот
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}
и вы можете издеваться network.default.источник
import * as obj
только в тесте или также в своем обычном коде?[method_name] is not declared writable or has no setter
что имеет смысл, поскольку импорт es6 постоянен. Есть ли способ обхода?import
(в отличие от тогоrequire
, который может идти куда угодно) поднимается, поэтому технически вы не можете импортировать несколько раз. Похоже, вашего шпиона зовут в другое место? Чтобы не дать тестам ошибиться в состоянии (известном как тестовое загрязнение), вы можете сбросить шпионов в afterEach (например, sinon.sandbox). Я считаю, что Жасмин делает это автоматически.import
на своем JS, на самом деле не используют модули ES6. Что-то вроде webpack или babel вмешается во время сборки и преобразует его либо в свой собственный внутренний механизм для вызова удаленных частей кода (например__webpack_require__
), либо в один из стандартов де-факто до ES6 , CommonJS, AMD или UMD. И это преобразование часто не соответствует спецификации. Так что для многих, многих разработчиков сейчас этот ответ работает нормально. На данный момент.carpeliam верен , но обратите внимание, что если вы хотите шпионить за функцией в модуле и использовать другую функцию в этом модуле, вызывающую эту функцию, вам необходимо вызвать эту функцию как часть пространства имен экспорта, иначе шпион не будет использоваться .
Неправильный пример:
// 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 }); });
источник
exports.myfunc2
- это прямая ссылка,myfunc2
пока неspyOn
заменяет ее ссылкой на шпионскую функцию.spyOn
изменит значениеexports.myfunc2
и заменит его на объект-шпион, в то время какmyfunc2
остается нетронутым в области видимости модуля (потому чтоspyOn
не имеет к нему доступа)*
замораживанием объекта, а атрибуты объекта нельзя изменить?export function
вместе сexports.myfunc2
технически смешивает синтаксис модулей commonjs и ES6, и это недопустимо в более новых версиях webpack (2+), которые требуют использования синтаксиса модуля ES6 по принципу «все или ничего». Я добавил ниже ответ, основанный на этом, который будет работать в строгих средах ES6.Ответ vdloo направил меня в правильном направлении, но использование ключевых слов «экспорт» CommonJS и модуля ES6 «экспорт» в одном файле у меня не сработало ( 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); }); });
источник
let MyModule
не требуется использовать экспорт по умолчанию (это может быть необработанный объект). Кроме того, этот метод не требуетmyfunc1()
вызоваmyfunc2()
, он работает, чтобы просто шпионить за ним напрямую.Я реализовал библиотеку, которая пытается решить проблему имитации импорта классов TypeScript во время выполнения, не требуя, чтобы исходный класс знал о какой-либо явной инъекции зависимостей.
Библиотека использует
import * as
синтаксис, а затем заменяет исходный экспортированный объект классом-заглушкой. Он сохраняет безопасность типов, поэтому ваши тесты будут прерваны во время компиляции, если имя метода было обновлено без обновления соответствующего теста.Эту библиотеку можно найти здесь: ts-mock-imports .
источник
Я обнаружил, что этот синтаксис работает:
Мой модуль:
// 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); }); });
См. Документацию .
источник
jest.mock()
соответствовать имени, используемому в import / packge.json, а не имени константы. В документации они оба одинаковы, но с таким кодом, какimport jwt from 'jsonwebtoken'
вам нужно настроить mock asjest.mock('jsonwebtoken')
Сам не пробовал, но думаю, что издевательство может сработать. Это позволяет вам заменить настоящий модуль предоставленным вами макетом. Ниже приведен пример, чтобы дать вам представление о том, как это работает:
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, но, тем не менее, это отличное решение для имитации модулей, которые иначе сложно имитировать.источник
Я недавно обнаружил babel-plugin-mockable-import, который аккуратно решает эту проблему, ИМХО. Если вы уже используете Babel , стоит изучить его.
источник
См. Предположим, я хотел бы имитировать результаты, возвращаемые
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(); }
источник