Как я могу издеваться над зависимостями для модульного тестирования в RequireJS?
У меня есть модуль AMD, который я хочу протестировать, но я хочу издеваться над его зависимостями, а не загружать фактические зависимости. Я использую requirejs, а код для моего модуля выглядит примерно так:
define(['hurp', 'durp'], function(Hurp, Durp) {
return {
foo: function () {
console.log(Hurp.beans)
},
bar: function () {
console.log(Durp.beans)
}
}
}
Как я могу издеваться над hurp
и durp
, чтобы эффективно unit test?
Ответы
Ответ 1
Итак, прочитав этот пост, я придумал решение, использующее функцию конфигурации requirejs для создания нового контекста для вашего теста, где вы можете просто высмеять ваши зависимости:
var cnt = 0;
function createContext(stubs) {
cnt++;
var map = {};
var i18n = stubs.i18n;
stubs.i18n = {
load: sinon.spy(function(name, req, onLoad) {
onLoad(i18n);
})
};
_.each(stubs, function(value, key) {
var stubName = 'stub' + key + cnt;
map[key] = stubName;
define(stubName, function() {
return value;
});
});
return require.config({
context: "context_" + cnt,
map: {
"*": map
},
baseUrl: 'js/cfe/app/'
});
}
Таким образом, он создает новый контекст, в котором определения для Hurp
и Durp
будут заданы объектами, которые вы передали в функцию. Math.random для названия, возможно, немного грязный, но он работает. Причина, если у вас будет куча теста, вам нужно создать новый контекст для каждого набора, чтобы предотвратить повторное использование ваших макетов или загрузить макеты, если вы хотите использовать настоящий модуль requirejs.
В вашем случае это будет выглядеть так:
(function () {
var stubs = {
hurp: 'hurp',
durp: 'durp'
};
var context = createContext(stubs);
context(['yourModuleName'], function (yourModule) {
//your normal jasmine test starts here
describe("yourModuleName", function () {
it('should log', function(){
spyOn(console, 'log');
yourModule.foo();
expect(console.log).toHasBeenCalledWith('hurp');
})
});
});
})();
Итак, я использую этот подход в производстве на некоторое время и его действительно надежный.
Ответ 2
вы можете проверить новый Squire.js lib
из документов:
Squire.js - это инжектор зависимостей для пользователей Require.js, чтобы упростить насмешливые зависимости!
Ответ 3
Я нашел три различных решения этой проблемы, ни один из них не приятен.
Определение зависимостей Inline
define('hurp', [], function () {
return {
beans: 'Beans'
};
});
define('durp', [], function () {
return {
beans: 'durp beans'
};
});
require('hurpdhurp', function () {
// test hurpdurp in here
});
Fugly. Вы должны загромождать свои тесты множеством шаблонов AMD.
Загрузка Mock-зависимостей с разных путей
Это связано с использованием отдельного файла config.js для определения путей для каждой из зависимостей, которые указывают на mocks вместо исходных зависимостей. Это также уродливо, требуя создания множества тестовых файлов и файлов конфигураций.
Подделка в Node
Это мое текущее решение, но все еще ужасное.
Вы создаете свою собственную функцию define
, чтобы предоставить свои собственные макеты модулю и поместить свои тесты в обратный вызов. Затем вы eval
модуль, чтобы выполнить ваши тесты, например:
var fs = require('fs')
, hurp = {
beans: 'BEANS'
}
, durp = {
beans: 'durp beans'
}
, hurpDurp = fs.readFileSync('path/to/hurpDurp', 'utf8');
;
function define(deps, cb) {
var TestableHurpDurp = cb(hurp, durp);
// now run tests below on TestableHurpDurp, which is using your
// passed-in mocks as dependencies.
}
// evaluate the AMD module, running your mocked define function and your tests.
eval(hurpDurp);
Это мое предпочтительное решение. Это выглядит немного волшебным, но у него есть несколько преимуществ.
- Запустите ваши тесты в node, поэтому не путайтесь с автоматизацией браузера.
- Меньше необходимости для грязного шаблона AMD в ваших тестах.
- Вы можете использовать
eval
в гневе и представить себе, как Крокфорд взрывается с яростью.
У него все еще есть некоторые недостатки, очевидно.
- Поскольку вы тестируете в node, вы ничего не можете делать с событиями браузера или манипуляциями с DOM. Только полезно для тестирования логики.
- Все еще немного неудобно настраивать. Вам нужно издеваться над
define
в каждом тесте, так как именно там ваши тесты действительно выполняются.
Я работаю над тестовым бегуном, чтобы дать хороший синтаксис для такого рода вещей, но у меня все еще нет хорошего решения проблемы 1.
Заключение
Отказывание отложений в requirejs сильно засасывает. Я нашел способ, который сортирует, но я все еще не очень доволен этим. Пожалуйста, дайте мне знать, если у вас есть лучшие идеи.
Ответ 4
Здесь есть опция config.map
http://requirejs.org/docs/api.html#config-map.
О том, как использовать его:
- Определить нормальный модуль;
- Определить модуль заглушки;
-
Настроить RequireJS на неопределенный срок;
requirejs.config({
map: {
'source/js': {
'foo': 'normalModule'
},
'source/test': {
'foo': 'stubModule'
}
}
});
В этом случае для нормального и тестового кода вы можете использовать модуль foo
, который будет представлять собой реальную ссылку на модуль и заглушку соответственно.
Ответ 5
Вы можете использовать testr.js для издевательства зависимостей. Вы можете установить testr для загрузки макетных зависимостей вместо исходных. Вот пример использования:
var fakeDep = function(){
this.getText = function(){
return 'Fake Dependancy';
};
};
var Module1 = testr('module1', {
'dependancies/dependancy1':fakeDep
});
Посмотрите также: http://cyberasylum.janithw.com/mocking-requirejs-dependencies-for-unit-testing/
Ответ 6
Этот ответ основан на ответе Андреаса Кёберле.
Мне было нелегко реализовать и понять его решение, поэтому я расскажу немного подробнее о том, как это работает, и о некоторых подводных камнях, которые следует избегать, надеясь, что это поможет будущим посетителям.
Итак, прежде всего настройка:
Я использую Karma как тестовый бегун и MochaJs как тестовую среду.
Использование чего-то типа Squire не помогло мне, по какой-то причине, когда я его использовал, тестовая среда бросила ошибки:
TypeError: Невозможно прочитать свойство "вызов" undefined
RequireJs имеет возможность map id модуля для других идентификаторов модулей. Это также позволяет создать require
функцию, в которой используется different config, чем глобальный require
.
Эти функции имеют решающее значение для работы этого решения.
Вот моя версия макет кода, в том числе (много) комментариев (я надеюсь, что это понятно). Я завернул его в модуль, так что тесты могут легко потребовать его.
define([], function () {
var count = 0;
var requireJsMock= Object.create(null);
requireJsMock.createMockRequire = function (mocks) {
//mocks is an object with the module ids/paths as keys, and the module as value
count++;
var map = {};
//register the mocks with unique names, and create a mapping from the mocked module id to the mock module id
//this will cause RequireJs to load the mock module instead of the real one
for (property in mocks) {
if (mocks.hasOwnProperty(property)) {
var moduleId = property; //the object property is the module id
var module = mocks[property]; //the value is the mock
var stubId = 'stub' + moduleId + count; //create a unique name to register the module
map[moduleId] = stubId; //add to the mapping
//register the mock with the unique id, so that RequireJs can actually call it
define(stubId, function () {
return module;
});
}
}
var defaultContext = requirejs.s.contexts._.config;
var requireMockContext = { baseUrl: defaultContext.baseUrl }; //use the baseUrl of the global RequireJs config, so that it doesn't have to be repeated here
requireMockContext.context = "context_" + count; //use a unique context name, so that the configs dont overlap
//use the mapping for all modules
requireMockContext.map = {
"*": map
};
return require.config(requireMockContext); //create a require function that uses the new config
};
return requireJsMock;
});
наибольшая ошибка, с которой я столкнулся, которая буквально стоила мне часов, создавала конфигурацию RequireJs. Я попытался (глубоко) скопировать его и только переопределить необходимые свойства (например, контекст или карту). Это не работает! Только скопируйте baseUrl
, это отлично работает.
Использование
Чтобы использовать его, попросите его в своем тесте, создайте mocks и передайте его createMockRequire
. Например:
var ModuleMock = function () {
this.method = function () {
methodCalled += 1;
};
};
var mocks = {
"ModuleIdOrPath": ModuleMock
}
var requireMocks = mocker.createMockRequire(mocks);
И вот пример полного тестового файла:
define(["chai", "requireJsMock"], function (chai, requireJsMock) {
var expect = chai.expect;
describe("Module", function () {
describe("Method", function () {
it("should work", function () {
return new Promise(function (resolve, reject) {
var handler = { handle: function () { } };
var called = 0;
var moduleBMock = function () {
this.method = function () {
methodCalled += 1;
};
};
var mocks = {
"ModuleBIdOrPath": moduleBMock
}
var requireMocks = requireJsMock.createMockRequire(mocks);
requireMocks(["js/ModuleA"], function (moduleA) {
try {
moduleA.method(); //moduleA should call method of moduleBMock
expect(called).to.equal(1);
resolve();
} catch (e) {
reject(e);
}
});
});
});
});
});
});
Ответ 7
если вы хотите сделать некоторые простые js-тесты, которые изолируют одну единицу, тогда вы можете просто использовать этот фрагмент:
function define(args, func){
if(!args.length){
throw new Error("please stick to the require.js api which wants a: define(['mydependency'], function(){})");
}
var fileName = document.scripts[document.scripts.length-1].src;
// get rid of the url and path elements
fileName = fileName.split("/");
fileName = fileName[fileName.length-1];
// get rid of the file ending
fileName = fileName.split(".");
fileName = fileName[0];
window[fileName] = func;
return func;
}
window.define = define;