¿Cómo acceder y probar una función interna (no exportable) en un módulo node.js?

181

Estoy tratando de descubrir cómo probar funciones internas (es decir, no exportadas) en nodejs (preferiblemente con mocha o jazmín). Y no tengo idea!

Digamos que tengo un módulo como ese:

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

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

exports.exported = exported;

Y la siguiente prueba (moca):

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

¿Hay alguna forma de probar la notExportedfunción de la unidad sin exportarla ya que no está destinada a ser expuesta?

xavier.seignard
fuente
1
¿Quizás solo exponga las funciones para probar en un entorno específico? No sé el procedimiento estándar aquí.
loganfsmyth

Respuestas:

243

El módulo de recableado es definitivamente la respuesta.

Aquí está mi código para acceder a una función no exportada y probarla usando 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();
  });
});
Antonio
fuente
2
Esta debería ser la mejor respuesta. No requiere reescribir todos los módulos existentes con exportaciones específicas de NODE_ENV, ni implica leer en el módulo como texto.
Adam Yost
Hermosa solución. Es posible ir más allá e integrarlo con espías en su marco de prueba. Trabajando con Jasmine, probé esta estrategia .
Franco
2
Gran solución ¿Existe una versión funcional para las personas de tipo Babel?
Charles Merriam
2
El uso de recableado con broma y broma ts-(mecanografiado) me sale el siguiente error: Cannot find module '../../package' from 'node.js'. ¿Has visto esto?
clu
2
Rewire tiene un problema de compatibilidad con broma. Jest no considerará las funciones llamadas desde el cableado en los informes de cobertura. Eso de alguna manera derrota el propósito.
robross0606
10

El truco consiste en establecer la NODE_ENVvariable de entorno en algo así testy luego exportarla condicionalmente.

Suponiendo que no ha instalado globalmente mocha, podría tener un Makefile en la raíz de su directorio de aplicaciones que contiene lo siguiente:

REPORTER = dot

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

.PHONY: test

Este archivo de configuración configura NODE_ENV antes de ejecutar mocha. Luego puede ejecutar sus pruebas de mocha con make testen la línea de comando.

Ahora, puede exportar condicionalmente su función que generalmente no se exporta solo cuando se ejecutan sus pruebas de 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;

La otra respuesta sugirió usar un módulo vm para evaluar el archivo, pero esto no funciona y arroja un error que indica que las exportaciones no están definidas.

Matthew Bradley
fuente
8
Esto parece un truco, ¿realmente no hay forma de probar las funciones internas (no exportadas) sin hacer eso si se bloquea NODE_ENV?
RyanHirsch
2
Eso es bastante desagradable. Esta no puede ser la mejor manera de resolver este problema.
npiv
7

EDITAR:

Cargar un módulo usando vmpuede causar un comportamiento inesperado (por ejemplo, el instanceofoperador ya no trabaja con objetos que se crean en dicho módulo porque los prototipos globales son diferentes de los utilizados en el módulo cargado normalmente require). Ya no uso la técnica a continuación y en su lugar uso el módulo de recableado . Funciona maravillosamente Aquí está mi respuesta original:

Desarrollando la respuesta de Srosh ...

Se siente un poco hacky, pero escribí un simple módulo "test_utils.js" que debería permitirle hacer lo que quiera sin tener exportaciones condicionales en sus módulos de aplicación:

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

Hay algunas cosas más que se incluyen en el moduleobjeto global de un módulo de nodo que también podrían necesitar entrar en el contextobjeto anterior, pero este es el conjunto mínimo que necesito para que funcione.

Aquí hay un ejemplo usando 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));
  });
});
mhess
fuente
2
¿Puedes dar un ejemplo de cómo accedes a una función no exportada rewire?
Matthias
1
Hola Matthias, te he dado un ejemplo haciendo exactamente eso en mi respuesta. Si te gusta, ¿tal vez votar un par de preguntas mías? :) Casi todas mis preguntas han quedado en 0 y StackOverflow está pensando en congelar mis preguntas. X_X
Anthony
2

Trabajando con Jasmine, traté de profundizar con la solución propuesta por Anthony Mayfield , basada en rewire .

Implementé la siguiente función ( Precaución : aún no se ha probado exhaustivamente, solo se ha compartido como una estrategia posible) :

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

Con una función como esta, podría espiar tanto los métodos de objetos no exportados como las funciones de nivel superior no exportadas, de la siguiente manera:

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

Entonces puede establecer expectativas como estas:

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

puede crear un nuevo contexto utilizando el módulo vm y evaluar el archivo js en él, algo así como lo hace réplica. entonces tienes acceso a todo lo que declara.

srosh
fuente
0

He encontrado una forma bastante simple que le permite probar, espiar y burlarse de esas funciones internas desde las pruebas:

Digamos que tenemos un módulo de nodo como este:

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

function myInternalFn() {

}

function myExportableFn() {
    myInternalFn();   
}

exports.myExportableFn = myExportableFn;

Si ahora queremos prueba y de espionaje y fingida myInternalFn mientras no exportarlo en la producción que tenemos que mejorar el archivo de la siguiente manera:

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;

Ahora puede probar, espiar y simular en myInternalFncualquier lugar donde lo use testable.myInternalFny en producción no se exporta .

heinob
fuente
0

Esta no es una práctica recomendada, pero si no puede usar rewirecomo lo sugiere @Antoine, siempre puede leer el archivo y usarlo eval().

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

Encontré esto útil mientras la unidad prueba archivos JS del lado del cliente para un sistema heredado.

Los archivos JS configurarían una gran cantidad de variables globales windowsin ninguna instrucción require(...)y module.exports(no había un paquete de módulos como Webpack o Browserify disponible para eliminar estas declaraciones de todos modos).

En lugar de refactorizar toda la base de código, esto nos permitió integrar pruebas unitarias en nuestro JS del lado del cliente.

Abhishek Divekar
fuente