Як отримати доступ та перевірити внутрішню (неекспортну) функцію в модулі node.js?


181

Я намагаюся розібратися, як перевірити внутрішні (тобто не експортовані) функції в 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));
    });
  });
});

Чи є спосіб перевірити notExportedфункцію, не фактично експортуючи її, оскільки вона не призначена для експонування?


1
Може бути, просто виставити функції для тестування в конкретному середовищі? Я не знаю тут стандартної процедури.
loganfsmyth

Відповіді:


243

Модуль перемотування , безумовно, є відповіддю.

Ось мій код доступу до не експортованої функції та тестування її за допомогою 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 не буде враховувати функції, викликані перемоткою в звітах про покриття. Це дещо перемагає мету.
robross0606

10

Трюк полягає в тому, щоб встановити 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 для оцінки файлу, але це не працює і видає помилку, вказуючи, що експорт не визначений.


8
Це здається злому, чи справді немає способу перевірити внутрішні (неекспортовані) функції, не роблячи цього, якщо заблокувати NODE_ENV?
RyanHirsch

2
Це досить неприємно. Це не може бути найкращим способом вирішення цієї проблеми.
npiv

7

Редагувати:

Завантаження модуля за допомогою vmможе спричинити несподівану поведінку (наприклад, instanceofоператор більше не працює з об'єктами, створеними в такому модулі, оскільки глобальні прототипи відрізняються від тих, які використовуються в модулі, завантаженому зазвичайrequire ). Я більше не використовую наведену нижче техніку і замість цього використовую модуль перемотування . Це чудово працює. Ось моя оригінальна відповідь:

Робота над грошовою відповіддю ...

Це здається трохи хитким, але я написав простий модуль "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 об'єкт який також може знадобитися перейти в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));
  });
});

2
ви можете навести приклад того, як ви отримуєте доступ до неекспортованої функції за допомогою rewire?
Маттіас

1
Гей, Маттіас, я наводив вам приклад, роблячи саме так у своїй відповіді. Якщо вам це подобається, можливо, визволите пару моїх питань? :) Майже всі мої запитання засіли в 0 і StackOverflow замислюється над тим, щоб заморозити мої запитання. X_X
Ентоні

2

Працюючи з Жасмін, я спробував заглибитися в рішення, запропоноване Ентоні Мейфілд , засноване на перезаписі .

Я реалізував таку функцію ( Увага : ще не ретельно перевірена, просто поділена як можлива стратегія) :

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 у ньому, як це робиться у репл. то ви маєте доступ до всього, що він декларує.


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у виробництві, він не експортується .


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.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.