Como acessar e testar uma função interna (não exportada) em um módulo node.js.

181

Estou tentando descobrir como testar funções internas (ou seja, não exportadas) em nodejs (de preferência com mocha ou jasmim). E eu não tenho ideia!

Digamos que eu tenho um módulo como esse:

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

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

exports.exported = exported;

E o seguinte teste (mocha):

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 alguma maneira de testar a notExportedfunção da unidade sem realmente exportá-la, pois ela não deve ser exposta?

xavier.seignard
fonte
1
Talvez apenas exponha as funções para testar quando estiver em um ambiente específico? Eu não sei o procedimento padrão aqui.
Loganfsmyth

Respostas:

243

O módulo de religação é definitivamente a resposta.

Aqui está o meu código para acessar uma função não exportada e testá-la usando o 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();
  });
});
Anthony
fonte
2
Essa deve ser absolutamente a melhor resposta. Não requer a reescrita de todos os módulos existentes com exportações específicas para NODE_ENV, nem envolve a leitura do módulo como texto.
Adam Yost
Solução bonita. É possível ir além e integrá-lo com espiões em sua estrutura de teste. Trabalhando com Jasmine, tentei essa estratégia .
Franco
2
Ótima solução. Existe uma versão funcional para pessoas do tipo Babel?
Charles Merriam
2
Usando religação com brincadeira e ts-brincadeira (datilografado) eu recebo o seguinte erro: Cannot find module '../../package' from 'node.js'. você viu isso?
clu
2
O Rewire tem um problema de compatibilidade com o gracejo. O Jest não considerará as funções chamadas de religar nos relatórios de cobertura. Isso derrota um pouco o propósito.
precisa saber é o seguinte
10

O truque é definir a NODE_ENVvariável de ambiente para algo como teste exportá-la condicionalmente.

Supondo que você não instalou o mocha globalmente, você pode ter um Makefile na raiz do diretório do seu aplicativo que contém o seguinte:

REPORTER = dot

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

.PHONY: test

Este arquivo make configura o NODE_ENV antes de executar o mocha. Você pode executar seus testes de mocha make testna linha de comando.

Agora, você pode exportar condicionalmente sua função que normalmente não é exportada apenas quando os testes do mocha estão em execução:

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;

A outra resposta sugeriu o uso de um módulo vm para avaliar o arquivo, mas isso não funciona e gera um erro informando que as exportações não estão definidas.

Matthew Bradley
fonte
8
Isso parece um hack, não há realmente nenhuma maneira de testar funções internas (não exportadas) sem fazer isso se o bloco NODE_ENV?
precisa saber é o seguinte
2
Isso é bem desagradável. Esta não pode ser a melhor maneira de resolver esse problema.
Npiv 23/10/14
7

EDITAR:

Carregar um módulo usando vmpode causar comportamento inesperado (por exemplo, o instanceofoperador não trabalha mais com objetos criados nesse módulo porque os protótipos globais são diferentes daqueles usados ​​no módulo carregado normalmente com ele require). Não uso mais a técnica abaixo e, em vez disso, uso o módulo de religação . Funciona maravilhosamente. Aqui está a minha resposta original:

Elaborando a resposta do srosh ...

Parece um pouco hacky, mas escrevi um módulo "test_utils.js" simples que deve permitir que você faça o que deseja sem ter exportações condicionais em seus módulos de aplicativo:

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

Há mais algumas coisas incluídas no moduleobjeto global de um módulo de nó que também podem precisar ir ao contextobjeto acima, mas esse é o conjunto mínimo necessário para que ele funcione.

Aqui está um exemplo usando o 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
fonte
2
você pode dar um exemplo de como você acessa uma função não exportada usando rewire?
Matthias
1
Ei Matthias, eu lhe dei um exemplo fazendo exatamente isso na minha resposta. Se você gostou, talvez vote algumas das minhas perguntas? :) Quase todas as minhas perguntas ficaram em 0 e o StackOverflow está pensando em congelar minhas perguntas. X_X
Anthony
2

Trabalhando com Jasmine, tentei ir mais fundo com a solução proposta por Anthony Mayfield , baseada em religar .

Eu implementei a seguinte função ( Cuidado : ainda não foi completamente testado, apenas compartilhado como uma estratégia possível) :

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

Com uma função como essa, você pode espionar os métodos de objetos não exportados e funções de nível superior não exportados, da seguinte maneira:

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

Então você pode definir expectativas como estas:

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

você pode criar um novo contexto usando o módulo vm e avaliar o arquivo js nele, como o repl faz. então você tem acesso a tudo o que declara.

srosh
fonte
0

Eu encontrei uma maneira bastante simples que permite testar, espionar e zombar dessas funções internas de dentro dos testes:

Digamos que temos um módulo de nó como este:

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

function myInternalFn() {

}

function myExportableFn() {
    myInternalFn();   
}

exports.myExportableFn = myExportableFn;

Se agora queremos teste e de espionagem e simulada myInternalFn embora não exportá-lo na produção , temos de melhorar o arquivo como este:

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;

Agora você pode testar, espionar e zombar em myInternalFnqualquer lugar em que o use como testable.myInternalFnna produção e não seja exportado .

heinob
fonte
0

Essa prática não é recomendada, mas se você não puder usar rewirecomo sugerido por @Antoine, sempre poderá ler o arquivo e usá-lo eval().

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

Achei isso útil durante o teste de unidade de arquivos JS do lado do cliente para um sistema legado.

Os arquivos JS iria criar uma série de variáveis globais sob windowsem qualquer require(...)emodule.exports instrução (não havia empacotador de módulo como o Webpack ou o Browserify disponíveis para remover essas instruções de qualquer maneira).

Em vez de refatorar toda a base de código, isso nos permitiu integrar testes de unidade em nosso JS do lado do cliente.

Abhishek Divekar
fonte