Как получить доступ и проверить внутреннюю (не экспортируемую) функцию в модуле node.js?

181

Я пытаюсь выяснить, как тестировать внутренние (то есть не экспортируемые) функции в nodejs (желательно с mocha или jasmine). И я понятия не имею!

Допустим, у меня есть такой модуль:

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));
    });
  });
});

Есть ли способ модульного тестирования notExportedфункции без ее фактического экспорта, поскольку она не предназначена для показа?

xavier.seignard
источник
1
Может быть, просто выставить функции для тестирования, когда в конкретной среде? Я не знаю стандартную процедуру здесь.
loganfsmyth

Ответы:

243

Модуль 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, а также не требует чтения в модуле в виде текста.
Адам Йост
Прекрасное решение. Можно пойти дальше и интегрировать его со шпионами в вашу тестовую среду. Работая с Жасмин, я попробовал эту стратегию .
Франко
2
Отличное решение. Есть ли рабочая версия для людей типа Вавилон?
Чарльз Мерриам
2
Использование ReWire с шуткой и TS-шутя (машинопись) Я получаю следующее сообщение об ошибке: Cannot find module '../../package' from 'node.js'. Вы видели это?
clu
2
Rewire имеет проблему совместимости с Jest. Jest не будет учитывать функции, вызванные из rewire, в отчетах о покрытии. Это несколько побеждает цель.
robross0606
10

Хитрость заключается в том, чтобы установить NODE_ENVпеременную окружения на что-то подобное, testа затем экспортировать ее условно.

Предполагая, что вы не установили глобальный mocha, вы можете иметь Makefile в корневом каталоге вашего приложения, который содержит следующее:

REPORTER = dot

test:
    @NODE_ENV=test ./node_modules/.bin/mocha \
        --recursive --reporter $(REPORTER) --ui bbd

.PHONY: test

Этот make-файл устанавливает NODE_ENV перед запуском mocha. Затем вы можете запустить свои тесты мокко сmake test помощью командной строки.

Теперь вы можете условно экспортировать свою функцию, которая обычно не экспортируется, только когда выполняются ваши тесты mocha:

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 для оценки файла, но это не работает и выдает ошибку, в которой говорится, что экспорт не определен.

Мэтью Брэдли
источник
8
Это похоже на хак, неужели нет способа протестировать внутренние (не экспортируемые) функции, не выполняя блок if if NODE_ENV?
RyanHirsch
2
Это довольно противно. Это не может быть лучшим способом решить эту проблему.
npiv
7

РЕДАКТИРОВАТЬ:

Загрузка модуля с использованием vmможет вызвать непредвиденное поведение (например, instanceofоператор больше не работает с объектами, созданными в таком модуле, поскольку глобальные прототипы отличаются от тех, которые используются в модуле, загружаемом обычно require). Я больше не использую описанную ниже технику и вместо этого использую модуль rewire . Это работает чудесно. Вот мой оригинальный ответ:

Разработка ответа Сроша ...

Это немного странно, но я написал простой модуль "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;};

Есть еще несколько вещей, которые включены в moduleобъект gobal модуля узла, которые также могут потребоватьсяcontext объект выше, но это минимальный набор, который мне нужен для его работы.

Вот пример использования мокко 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));
  });
});
mhess
источник
2
Можете ли вы привести пример, как вы получаете доступ к неэкспортированной функции с помощью rewire?
Матиас
1
Привет, Матиас, я привел тебе пример, делающий именно это в моем ответе. Если вам это нравится, может быть, upvote пару моих вопросов? :) Почти все мои вопросы находятся в 0, и StackOverflow думает о заморозке моих вопросов. X_X
Энтони
2

Работая с Жасмин, я попытался углубиться в решение, предложенное Энтони Мэйфилдом , на основе 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);
Франко
источник
0

Вы можете создать новый контекст, используя модуль vm, и оценить в нем js-файл, как в repl. тогда у вас есть доступ ко всему, что он заявляет.

srosh
источник
0

Я нашел довольно простой способ, который позволяет вам тестировать, шпионить и высмеивать эти внутренние функции из тестов:

Допустим, у нас есть модуль узла, подобный этому:

mymodule.js:
------------
"use strict";

function myInternalFn() {

}

function myExportableFn() {
    myInternalFn();   
}

exports.myExportableFn = myExportableFn;

Если мы теперь хотим проверить и шпионить и издеваться 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;

Теперь вы можете тестировать, шпионить и издеваться myInternalFnвезде, где вы используете его, а testable.myInternalFnв производстве он не экспортируется .

heinob
источник
0

Это не рекомендуемая практика, но если вы не можете использовать rewireкак предложено @Antoine, вы всегда можете просто прочитать файл и использовать eval().

var fs = require('fs');
const JsFileString = fs.readFileSync(fileAbsolutePath, 'utf-8');
eval(JsFileString);

Я нашел это полезным при модульном тестировании JS-файлов на стороне клиента для устаревшей системы.

В файлах JS будет установлено множество глобальных переменных windowбез каких-либо require(...)иmodule.exports заявлений (не было модуля пакетирования как Webpack или Browserify доступны , чтобы удалить эти заявления в любом случае).

Вместо рефакторинга всей кодовой базы это позволило нам интегрировать модульные тесты в наш клиентский JS.

Абхишек Дивекар
источник