Как получить доступ и проверить внутреннюю (не экспортную) функцию в модуле node.js?
Я пытаюсь выяснить, как тестировать внутренние (то есть не экспортируемые) функции в nodejs (желательно с моккой или жасмином). И я понятия не имею!
Скажем, у меня есть такой модуль:
function exported(i) {
return notExported(i) + 1;
}
function notExported(i) {
return i*2;
}
exports.exported = exported;
И следующий тест (мокко):
var assert = require('assert'),
test = require('../modules/core/test');
describe('test', function(){
describe('#exported(i)', function(){
it('should return (i*2)+1 for any given i', function(){
assert.equal(3, test.exported(1));
assert.equal(5, test.exported(2));
});
});
});
Есть ли способ unit test функции notExported
без фактического экспорта, поскольку он не предназначен для экспонирования?
Ответы
Ответ 1
Модуль rewire определенно является ответом.
Вот мой код для доступа к неэкспортурованной функции и ее тестирования с помощью Mocha.
application.js:
function logMongoError(){
console.error('MongoDB Connection Error. Please make sure that MongoDB is running.');
}
test.js:
var rewire = require('rewire');
var chai = require('chai');
var should = chai.should();
var app = rewire('../application/application.js');
logError = app.__get__('logMongoError');
describe('Application module', function() {
it('should output the correct error', function(done) {
logError().should.equal('MongoDB Connection Error. Please make sure that MongoDB is running.');
done();
});
});
Ответ 2
Трюк заключается в том, чтобы установить переменную среды NODE_ENV
на что-то вроде test
, а затем условно экспортировать ее.
Предполагая, что вы не установили мокко глобально, вы можете создать Makefile в корне вашего каталога приложений, который содержит следующее:
REPORTER = dot
test:
@NODE_ENV=test ./node_modules/.bin/mocha \
--recursive --reporter $(REPORTER) --ui bbd
.PHONY: test
Этот файл make устанавливает NODE_ENV перед запуском mocha. Затем вы можете запустить ваши тесты мокки с помощью make test
в командной строке.
Теперь вы можете условно экспортировать свою функцию, которая обычно не экспортируется, только когда выполняются ваши тесты мокко:
function exported(i) {
return notExported(i) + 1;
}
function notExported(i) {
return i*2;
}
if (process.env.NODE_ENV === "test") {
exports.notExported = notExported;
}
exports.exported = exported;
Другой ответ, предложенный с помощью модуля vm для оценки файла, но это не работает и выдает ошибку, в которой указано, что экспорт не определен.
Ответ 3
EDIT:
Загрузка модуля с помощью vm
может привести к неожиданному поведению (например, оператор instanceof
больше не работает с объектами, созданными в таком модуле, потому что глобальные прототипы отличаются от тех, которые используются в модуле, загружаемом обычно с помощью require
). Я больше не использую метод ниже и вместо этого использую модуль rewire. Он работает чудесно. Вот мой оригинальный ответ:
Разработка ответа srosh...
Он немного взломан, но я написал простой модуль "test_utils.js", который должен позволять вам делать то, что вы хотите, без условного экспорта в ваши прикладные модули:
var Script = require('vm').Script,
fs = require('fs'),
path = require('path'),
mod = require('module');
exports.expose = function(filePath) {
filePath = path.resolve(__dirname, filePath);
var src = fs.readFileSync(filePath, 'utf8');
var context = {
parent: module.parent, paths: module.paths,
console: console, exports: {}};
context.module = context;
context.require = function (file){
return mod.prototype.require.call(context, file);};
(new Script(src)).runInNewContext(context);
return context;};
Есть еще несколько вещей, которые включены в объект node gobal module
, который также может понадобиться перейти в объект context
выше, но это минимальный набор, который мне нужен для его работы.
Здесь пример использования mocha BDD:
var util = require('./test_utils.js'),
assert = require('assert');
var appModule = util.expose('/path/to/module/modName.js');
describe('appModule', function(){
it('should test notExposed', function(){
assert.equal(6, appModule.notExported(3));
});
});
Ответ 4
Я нашел довольно простой способ, который позволяет тестировать, отслеживать и издеваться над этими внутренними функциями из тестов:
Скажем, у нас есть модуль node:
mymodule.js:
------------
"use strict";
function myInternalFn() {
}
function myExportableFn() {
myInternalFn();
}
exports.myExportableFn = myExportableFn;
Если мы хотим протестировать и spy и mock myInternalFn
, не экспортируя его в производство, нам нужно улучшить файл, например это:
my_modified_module.js:
----------------------
"use strict";
var testable; // <-- this is new
function myInternalFn() {
}
function myExportableFn() {
testable.myInternalFn(); // <-- this has changed
}
exports.myExportableFn = myExportableFn;
// the following part is new
if( typeof jasmine !== "undefined" ) {
testable = exports;
} else {
testable = {};
}
testable.myInternalFn = myInternalFn;
Теперь вы можете тестировать, шпионить и mock myInternalFn
везде, где вы используете его как testable.myInternalFn
, а в процессе производства не экспортируется.
Ответ 5
вы можете создать новый контекст, используя vm модуль и eval файл js в нем, вроде как repl. то у вас есть доступ ко всему, что он объявляет.
Ответ 6
Работая с Жасмин, я попытался углубиться в решение предложенное Энтони Мэйфилдом, основанное на rewire.
Я реализовал следующую функцию ( Предостережение: еще не полностью протестировано, просто разделяется как возможная стратегия):
function spyOnRewired() {
const SPY_OBJECT = "rewired"; // choose preferred name for holder object
var wiredModule = arguments[0];
var mockField = arguments[1];
wiredModule[SPY_OBJECT] = wiredModule[SPY_OBJECT] || {};
if (wiredModule[SPY_OBJECT][mockField]) // if it was already spied on...
// ...reset to the value reverted by jasmine
wiredModule.__set__(mockField, wiredModule[SPY_OBJECT][mockField]);
else
wiredModule[SPY_OBJECT][mockField] = wiredModule.__get__(mockField);
if (arguments.length == 2) { // top level function
var returnedSpy = spyOn(wiredModule[SPY_OBJECT], mockField);
wiredModule.__set__(mockField, wiredModule[SPY_OBJECT][mockField]);
return returnedSpy;
} else if (arguments.length == 3) { // method
var wiredMethod = arguments[2];
return spyOn(wiredModule[SPY_OBJECT][mockField], wiredMethod);
}
}
С помощью такой функции вы можете отслеживать оба метода неэкспортируемых объектов и неэкспортированные функции верхнего уровня, как показано ниже:
var dbLoader = require("rewire")("../lib/db-loader");
// Example: rewired module dbLoader
// It has non-exported, top level object 'fs' and function 'message'
spyOnRewired(dbLoader, "fs", "readFileSync").and.returnValue(FULL_POST_TEXT); // method
spyOnRewired(dbLoader, "message"); // top level function
Затем вы можете установить ожидания следующим образом:
expect(dbLoader.rewired.fs.readFileSync).toHaveBeenCalled();
expect(dbLoader.rewired.message).toHaveBeenCalledWith(POST_DESCRIPTION);