Другими словами, как заглушить / шпионить за модулем, в котором экспортирована только одна функция?
// sum.js module function sum(x, y) { return x + y; } module.exports = sum;
Допустим, указанная выше функция импортируется в файл js следующим образом:
// doStuff.js module const sum = require('./sum'); function doStuff(x, y) { // do some stuff then ... return sum(x, y); } module.exports = doStuff;
Здесь нам нужно протестировать функцию doStuff, заглушив функцию sum.
Это возможно? Ну да. Однако это не так просто. Я пишу эту историю, чтобы объяснить, почему это непросто, и какие возможные варианты или инструменты мы можем использовать для этого.
Первое решение
>>>>>>>>
Если вы являетесь создателем этой функции sum, то простое решение здесь - изменить ее и изменить способ экспорта функции на одно из следующих решений:
module.exports.sum = sum; // or module.exports = { sum }; // or sometimes we might find it like module.exports.default = sum;
Здесь мы меняем модуль sum, чтобы экспортировать объект, содержащий функцию sum.
Затем вы можете писать заглушки с помощью Sinon следующим образом:
const { assert } = require('chai'); const sinon = require('sinon'); const sumModule = require('./sum'); const doStuff = require('./doStuff'); sinon.stub(sumModule, 'sum').returns('fake sum') const actual = doStuff(1, 2); assert.equals(actual, 'fake sum');
Но что, если я не могу изменить модуль сумма? Что, если я установил эту служебную функцию из библиотеки с открытым исходным кодом, и вы не можете ее изменить? Что ж, вы можете подумать о том, чтобы внести свой вклад в библиотеку с открытым исходным кодом. Или…
Вот второй вариант, который вы можете сделать.
Второе решение
>>>>>>>>>>
Установите proxyquire или mock-require. Да, как вы уже слышали, вам нужно установить библиотеку только для того, чтобы заглушить экспортируемую функцию по умолчанию.
const proxyquire = require('proxyquire') const sinon = require('sinon'); const sum = sinon.stub(); const moduleWithDependency = proxyquire('./doStuff’, { './sum': sum, }); moduleWithDependency(1, 2); sinon.assert.calledOnce(sum);
В том, что все? Нет. Есть еще одно решение - обойтись без дополнительных библиотек. Вот почему я пишу этот рассказ.
Третье решение
>>>>>>>>
Давайте сначала разберемся с проблемой, которую мы здесь пытаемся исправить. Узел импортирует модули один раз, а затем кеширует их. После кеширования он не импортирует их снова.
Вы можете легко это попробовать, выполнив следующие действия.
В начале любого JS файла напишите:
// module name: test-module.js console.log('module loaded'); // some other code here
и потребовать этот файл дважды в другом файле.
require('./test-module'); require('./test-module');
Вы увидите, что консоль печатает «модуль загружен» только один раз, а не два раза.
Почему?
Так работает Node. После запуска приложения узел загружает экспортированные модули один раз и кэширует их в этом объекте module.cache. Здесь Node отслеживает все загруженные модули commonjs.
К счастью, этот объект доступен для записи, поэтому мы можем редактировать или удалять загруженные модули следующим образом:
delete module.cache[require.resolve(module-name)];
Вы можете проверить это, потребовав один и тот же модуль дважды снова, но на этот раз напишите оператор удаления посередине следующим образом
require('module-name'); delete module.cache[require.resolve('module-name')]; require('module-name');
На этот раз вы увидите, что консоль дважды напечатает «модуль загружен».
Какая связь между этой загрузкой модуля и заглушкой экспортируемой функции по умолчанию?
Node будет загружать и кэшировать экспортируемую функцию по умолчанию, поэтому никакая библиотека-заглушка, такая как sinon, не сможет подделать / шпионить за ней, если мы снова не перезагрузим модуль в объекте кеша. Как?
// First we need to remove the doStuff module delete require.cache[require.resolve('./doStuff')]; // Second we need rewrite the cached sum module to be as follows: require.cache[require.resolve('./sum')] = { exports: sinon.stub(), }; // Third we need to require the doStuff module again require('./doStuff');
Вот и все.
Но что, если функция, которую я тестирую, вызывает 2 или более функций по умолчанию из разных модулей? Я хочу заглушить все эти экспортируемые по умолчанию функции.
Что ж, в этом случае давайте перепишем три вышеупомянутые строки в функции, которая автоматизирует это для нас, следующим образом:
function fakeDefaultExport(moduleRelativePath, stubs) => { if (require.cache[require.resolve(moduleRelativePath)]) { delete require.cache[require.resolve(moduleRelativePath)]; } Object.keys(stubs).forEach(dependencyRelativePath => { require.cache[require.resolve(dependencyRelativePath)] = { exports: stubs[dependencyRelativePath], }; }); return require(moduleRelativePath); };
… И мы можем использовать его следующим образом:
const doStuff = fakeDefaultExport("./doStuff", { './sum': sinon.stub().returns('fake adding'), './sub': sinon.stub().returns('fake subtraction'), });
Надеюсь, тебе понравится.