Wie kann ich auf eine interne Funktion (ohne Export) in einem node.js-Modul zugreifen und diese testen?


178

Ich versuche herauszufinden, wie man interne (dh nicht exportierte) Funktionen in NodeJs testet (vorzugsweise mit Mokka oder Jasmin). Und ich habe keine Ahnung!

Angenommen, ich habe ein solches Modul:

function exported(i) {
   return notExported(i) + 1;
}

function notExported(i) {
   return i*2;
}

exports.exported = exported;

Und der folgende Test (Mokka):

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

Gibt es eine Möglichkeit, die notExportedFunktion zu testen , ohne sie tatsächlich zu exportieren, da sie nicht für die Belichtung vorgesehen ist?


1
Vielleicht stellen Sie einfach die zu testenden Funktionen in einer bestimmten Umgebung zur Verfügung? Ich kenne hier kein Standardverfahren.
Loganfsmyth

Antworten:


239

Das Rewire- Modul ist definitiv die Antwort.

Hier ist mein Code für den Zugriff auf eine nicht exportierte Funktion und das Testen mit 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
Dies sollte unbedingt die beste Antwort sein. Es ist nicht erforderlich, alle vorhandenen Module mit NODE_ENV-spezifischen Exporten neu zu schreiben, und das Modul muss auch nicht als Text eingelesen werden.
Adam Yost

Schöne Lösung. Es ist möglich, noch weiter zu gehen und es mit Spionen in Ihr Test-Framework zu integrieren. In Zusammenarbeit mit Jasmine habe ich diese Strategie ausprobiert .
Franco

2
Tolle Lösung. Gibt es eine funktionierende Version für Leute vom Typ Babel?
Charles Merriam

2
Bei Verwendung von rewire mit jest und ts-jest (Typoskript) wird folgende Fehlermeldung angezeigt : Cannot find module '../../package' from 'node.js'. Hast du das gesehen?
Clu

2
Rewire hat ein Kompatibilitätsproblem mit Jest. Jest berücksichtigt keine Funktionen, die von Rewire in Abdeckungsberichten aufgerufen werden. Das macht den Zweck etwas zunichte.
Robross0606

10

Der Trick besteht darin, die NODE_ENVUmgebungsvariable auf so etwas wie zu setzen testund sie dann bedingt zu exportieren.

Angenommen, Sie haben Mokka nicht global installiert, könnte sich im Stammverzeichnis Ihres App-Verzeichnisses ein Makefile befinden, das Folgendes enthält:

REPORTER = dot

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

.PHONY: test

Diese make-Datei richtet NODE_ENV ein, bevor Mokka ausgeführt wird. Sie können dann Ihre Mokka-Tests mit make testüber die Befehlszeile ausführen .

Jetzt können Sie Ihre Funktion bedingt exportieren, die normalerweise nicht nur exportiert wird, wenn Ihre Mokka-Tests ausgeführt werden:

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;

Die andere Antwort schlug vor, ein VM-Modul zum Auswerten der Datei zu verwenden, dies funktioniert jedoch nicht und es wird ein Fehler ausgegeben, der besagt, dass Exporte nicht definiert sind.


8
Dies scheint ein Hack zu sein. Gibt es wirklich keine Möglichkeit, interne (nicht exportierte) Funktionen zu testen, ohne einen That-If-NODE_ENV-Block auszuführen?
Ryan Hirsch

2
Das ist ziemlich böse. Dies kann nicht der beste Weg sein, um dieses Problem zu lösen.
npiv

7

BEARBEITEN:

Das Laden eines Moduls mit vmkann zu unerwartetem Verhalten führen (z. B. arbeitet der instanceofBediener nicht mehr mit Objekten, die in einem solchen Modul erstellt wurden, da sich die globalen Prototypen von denen unterscheiden, die in Modulen verwendet werden, die normalerweise mit geladen werden require). Ich verwende die folgende Technik nicht mehr und verwende stattdessen das Umverdrahtungsmodul . Es funktioniert wunderbar. Hier ist meine ursprüngliche Antwort:

Ausarbeitung der Antwort von srosh ...

Es fühlt sich ein bisschen hackig an, aber ich habe ein einfaches "test_utils.js" -Modul geschrieben, mit dem Sie tun können, was Sie wollen, ohne bedingte Exporte in Ihren Anwendungsmodulen zu haben:

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

Es gibt einige weitere Dinge, die im globalen moduleObjekt eines Knotenmoduls enthalten sind und möglicherweise auch in das contextobige Objekt aufgenommen werden müssen, aber dies ist die Mindestmenge, die ich benötige, damit es funktioniert.

Hier ist ein Beispiel mit Mokka 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
Können Sie ein Beispiel geben, wie Sie mit auf eine nicht exportierte Funktion zugreifen rewire?
Matthias

1
Hey Matthias, ich habe dir in meiner Antwort ein Beispiel gegeben, das genau das tut. Wenn es Ihnen gefällt, stimmen Sie vielleicht ein paar Fragen von mir ab? :) Fast alle meine Fragen haben bei 0 gestanden und StackOverflow denkt darüber nach, meine Fragen einzufrieren. X_X
Anthony

2

In Zusammenarbeit mit Jasmine habe ich versucht, die von Anthony Mayfield vorgeschlagene Lösung basierend auf der Neuverdrahtung zu vertiefen .

Ich habe die folgende Funktion implementiert ( Achtung : noch nicht gründlich getestet, nur als mögliche Strategie geteilt) :

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

Mit einer solchen Funktion können Sie sowohl Methoden nicht exportierter Objekte als auch nicht exportierter Funktionen der obersten Ebene wie folgt ausspionieren:

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

Dann können Sie folgende Erwartungen setzen:

expect(dbLoader.rewired.fs.readFileSync).toHaveBeenCalled();
expect(dbLoader.rewired.message).toHaveBeenCalledWith(POST_DESCRIPTION);

1

Ich habe einen recht einfachen Weg gefunden, mit dem Sie diese internen Funktionen innerhalb der Tests testen, ausspionieren und verspotten können:

Angenommen, wir haben ein Knotenmodul wie das folgende:

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

function myInternalFn() {

}

function myExportableFn() {
    myInternalFn();   
}

exports.myExportableFn = myExportableFn;

Wenn wir jetzt testen und ausspionieren und verspotten möchten, myInternalFn ohne es in der Produktion zu exportieren, müssen wir die Datei wie folgt verbessern:

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;

Jetzt können Sie myInternalFnüberall dort testen, ausspionieren und verspotten , wo Sie es verwenden, testable.myInternalFnund in der Produktion wird es nicht exportiert .


0

Sie können mit dem VM- Modul einen neuen Kontext erstellen und die darin enthaltene JS-Datei auswerten, ähnlich wie bei Repl. dann haben Sie Zugriff auf alles, was es deklariert.


0

Dies wird nicht empfohlen, aber wenn Sie nicht rewirewie von @Antoine vorgeschlagen verwenden können, können Sie die Datei immer nur lesen und verwenden eval().

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

Ich fand dies nützlich, als ich clientseitige JS-Dateien für ein Legacy-System testete.

Die JS-Dateien würden viele globale Variablen windowohne require(...)und module.exportsAnweisungen einrichten (es war ohnehin kein Modulbündler wie Webpack oder Browserify verfügbar, um diese Anweisungen zu entfernen).

Anstatt die gesamte Codebasis umzugestalten, konnten wir Unit-Tests in unsere clientseitige JS integrieren.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.