Comment accéder et tester une fonction interne (non exportée) dans un module node.js?


181

J'essaie de comprendre comment tester les fonctions internes (c'est-à-dire non exportées) dans nodejs (de préférence avec moka ou jasmine). Et je n'ai aucune idée!

Disons que j'ai un module comme ça:

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

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

exports.exported = exported;

Et le test suivant (moka):

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

Existe-t-il un moyen de tester l'unité la notExportedfonction sans l'exporter, car elle n'est pas destinée à être exposée?


1
Peut-être simplement exposer les fonctions à tester dans un environnement spécifique? Je ne connais pas la procédure standard ici.
loganfsmyth

Réponses:


243

Le module rewire est définitivement la réponse.

Voici mon code pour accéder à une fonction non exportée et la tester à l'aide de 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
Cela devrait absolument être la meilleure réponse. Cela ne nécessite pas de réécrire tous les modules existants avec des exportations spécifiques à NODE_ENV, ni de lire le module sous forme de texte.
Adam Yost

Belle solution. Il est possible d'aller plus loin et de l'intégrer avec des espions dans votre framework de test. En travaillant avec Jasmine, j'ai essayé cette stratégie .
Franco

2
Excellente solution. Existe-t-il une version de travail pour les personnes de type Babel?
Charles Merriam

2
L' utilisation ReWire avec plaisanterie et ts-plaisanterie (dactylographiée) Je reçois l'erreur suivante: Cannot find module '../../package' from 'node.js'. Avez-vous vu cette?
clu

2
Rewire a un problème de compatibilité avec jest. Jest ne prendra pas en compte les fonctions appelées à partir de rewire dans les rapports de couverture. Cela va quelque peu à l'encontre de l'objectif.
robross0606

10

L'astuce consiste à définir la NODE_ENVvariable d'environnement sur quelque chose comme test, puis à l'exporter conditionnellement.

En supposant que vous n'ayez pas installé globalement mocha, vous pourriez avoir un Makefile à la racine de votre répertoire d'application qui contient les éléments suivants:

REPORTER = dot

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

.PHONY: test

Ce fichier make configure le NODE_ENV avant d'exécuter mocha. Vous pouvez ensuite exécuter vos tests de moka avecmake test en ligne de commande.

Maintenant, vous pouvez exporter conditionnellement votre fonction qui n'est généralement pas exportée uniquement lorsque vos tests mocha sont en cours d'exécution:

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;

L'autre réponse suggérait d'utiliser un module vm pour évaluer le fichier, mais cela ne fonctionne pas et génère une erreur indiquant que les exportations ne sont pas définies.


8
Cela semble être un hack, n'y a-t-il vraiment aucun moyen de tester les fonctions internes (non exportées) sans faire un bloc that if NODE_ENV?
RyanHirsch

2
C'est assez méchant. Cela ne peut pas être la meilleure façon de résoudre ce problème.
npiv

7

ÉDITER:

Le chargement d'un module à l'aide de vmpeut provoquer un comportement inattendu (par exemple, l' instanceofopérateur ne travaille plus avec les objets créés dans un tel module car les prototypes globaux sont différents de ceux utilisés dans le module chargé normalement avec require). Je n'utilise plus la technique ci-dessous et utilise à la place le module rewire . Cela fonctionne à merveille. Voici ma réponse originale:

Élaborer sur la réponse de srosh ...

Cela semble un peu hacky, mais j'ai écrit un simple module "test_utils.js" qui devrait vous permettre de faire ce que vous voulez sans avoir d'exportations conditionnelles dans vos modules d'application:

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

Il y a d'autres choses qui sont incluses dans l' moduleobjet global d'un module de nœud qui pourraient également avoir besoin d'aller dans lecontext objet ci-dessus, mais c'est l'ensemble minimum dont j'ai besoin pour que cela fonctionne.

Voici un exemple utilisant le moka 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
pouvez-vous donner un exemple comment accéder à une fonction non exportée en utilisant rewire?
Matthias

1
Hey Matthias, je vous ai donné un exemple faisant exactement cela dans ma réponse. Si vous l'aimez, peut-être voter pour quelques-unes de mes questions? :) Presque toutes mes questions sont restées à 0 et StackOverflow envisage de geler mes questions. X_X
Anthony

2

En travaillant avec Jasmine, j'ai essayé d'aller plus loin avec la solution proposée par Anthony Mayfield , basée sur rewire .

J'ai implémenté la fonction suivante ( Attention : pas encore complètement testée, juste partagée comme stratégie possible) :

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

Avec une fonction comme celle-ci, vous pouvez espionner les deux méthodes d'objets non exportés et les fonctions de niveau supérieur non exportées, comme suit:

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

Ensuite, vous pouvez définir des attentes comme celles-ci:

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

0

vous pouvez créer un nouveau contexte en utilisant le module vm et évaluer le fichier js qu'il contient, un peu comme le fait repl. alors vous avez accès à tout ce qu'il déclare.


0

J'ai trouvé un moyen assez simple qui vous permet de tester, d'espionner et de se moquer de ces fonctions internes à partir des tests:

Disons que nous avons un module de nœud comme celui-ci:

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

function myInternalFn() {

}

function myExportableFn() {
    myInternalFn();   
}

exports.myExportableFn = myExportableFn;

Si nous voulons maintenant tester et espionner et nous moquer myInternalFn sans l'exporter en production, nous devons améliorer le fichier comme ceci:

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;

Maintenant, vous pouvez tester, espionner et vous moquer myInternalFnpartout où vous l'utilisez au fur testable.myInternalFnet à mesure et en production, il n'est pas exporté .


0

Ce n'est pas une pratique recommandée, mais si vous ne pouvez pas utiliser rewirecomme suggéré par @Antoine, vous pouvez toujours simplement lire le fichier et l'utiliser eval().

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

J'ai trouvé cela utile lors des tests unitaires de fichiers JS côté client pour un système hérité.

Les fichiers JS mettraient en place un grand nombre de variables globales sous windowsans aucune instruction require(...)et module.exports(il n'y avait pas de bundler de module comme Webpack ou Browserify disponible pour supprimer ces instructions de toute façon).

Plutôt que de refactoriser toute la base de code, cela nous a permis d'intégrer des tests unitaires dans notre JS côté client.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.