Как получить доступ и проверить внутреннюю (не экспортную) функцию в модуле 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);