Jak uzyskać dostęp i przetestować wewnętrzną (nie eksportującą) funkcję w module node.js?

181

Próbuję dowiedzieć się, jak przetestować wewnętrzne (tj. Nie eksportowane) funkcje w nodejs (najlepiej z mokką lub jaśminem). I nie mam pojęcia!

Powiedzmy, że mam taki moduł:

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

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

exports.exported = exported;

I następujący 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));
    });
  });
});

Czy istnieje sposób na przetestowanie notExportedfunkcji bez jej eksportowania, ponieważ nie jest przeznaczona do ujawnienia?

xavier.seignard
źródło
1
Może po prostu ujawnij funkcje do przetestowania w określonym środowisku? Nie znam tutaj standardowej procedury.
loganfsmyth,

Odpowiedzi:

243

ReWire moduł jest na pewno odpowiedź.

Oto mój kod dostępu do niewyeksportowanej funkcji i testowania jej za pomocą 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
źródło
2
To powinna być absolutnie najlepsza odpowiedź. Nie wymaga przepisywania wszystkich istniejących modułów z eksportami specyficznymi dla NODE_ENV, ani nie wymaga czytania modułu jako tekstu.
Adam Yost
Piękne rozwiązanie. Istnieje możliwość pójścia dalej i zintegrowania go ze szpiegami we frameworku testowym. Pracując z Jasmine, wypróbowałem tę strategię .
Franco
2
Świetne rozwiązanie. Czy istnieje działająca wersja dla ludzi typu Babel?
Charles Merriam
2
Korzystanie z ReWire JEST i TS-żartem (maszynopis) pojawia się następujący błąd: Cannot find module '../../package' from 'node.js'. Widziałeś to?
clu
2
Rewire ma problem ze zgodnością z żartem. Nie będzie uwzględniać funkcji wywoływanych z rewire w raportach pokrycia. To trochę przeczy celowi.
robross0606
10

Sztuczka polega na ustawieniu NODE_ENVzmiennej środowiskowej na coś podobnego, testa następnie warunkowym wyeksportowaniu.

Zakładając, że nie masz globalnie zainstalowanej mokki, możesz mieć plik Makefile w katalogu głównym aplikacji, który zawiera następujące elementy:

REPORTER = dot

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

.PHONY: test

Ten plik make ustawia NODE_ENV przed uruchomieniem mokki. Następnie możesz uruchomić testy mokki make testw wierszu poleceń.

Teraz możesz warunkowo wyeksportować swoją funkcję, która zwykle nie jest eksportowana tylko wtedy, gdy są uruchomione testy mokka:

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;

Druga odpowiedź sugerowała użycie modułu maszyny wirtualnej do oceny pliku, ale to nie działa i generuje błąd informujący, że eksport nie jest zdefiniowany.

Matthew Bradley
źródło
8
Wygląda na to, że to hack, czy naprawdę nie ma sposobu na przetestowanie wewnętrznych (nieeksportowanych) funkcji bez zrobienia tego, jeśli NODE_ENV blokuje?
RyanHirsch
2
To dość paskudne. To nie może być najlepszy sposób rozwiązania tego problemu.
npiv
7

EDYTOWAĆ:

Ładowanie modułu za pomocą vmmoże spowodować nieoczekiwane zachowanie (np. instanceofOperator nie pracuje już z obiektami, które są tworzone w takim module, ponieważ prototypy globalne różnią się od tych używanych w module ładowanym normalnie require). Nie używam już poniższej techniki i zamiast tego używam modułu rewire . Działa wspaniale. Oto moja oryginalna odpowiedź:

Rozwijając odpowiedź Srosha ...

Wydaje się to trochę hakerskie, ale napisałem prosty moduł "test_utils.js", który powinien pozwolić ci robić to, co chcesz, bez warunkowego eksportowania w modułach aplikacji:

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

Jest jeszcze kilka rzeczy, które są zawarte w moduleobiekcie globalnym modułu węzła, które mogą również wymagać przejścia do contextpowyższego obiektu, ale jest to minimalny zestaw, którego potrzebuję, aby działał.

Oto przykład użycia mokki 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
źródło
2
czy możesz podać przykład, w jaki sposób uzyskujesz dostęp do niewyeksportowanej funkcji za pomocą rewire?
Matthias
1
Hej Matthias, podałem ci przykład, robiąc dokładnie to w mojej odpowiedzi. Jeśli ci się spodoba, może zagłosuj na kilka moich pytań? :) Prawie wszystkie moje pytania osiągnęły wartość 0, a StackOverflow myśli o zamrożeniu moich pytań. X_X
Anthony
2

Współpracując z Jasmine, starałem się wejść głębiej w rozwiązanie zaproponowane przez Anthony'ego Mayfielda , oparte na rewire .

Zaimplementowałem następującą funkcję ( Uwaga : jeszcze nie do końca przetestowana, tylko udostępniona jako możliwa strategia) :

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

Dzięki takiej funkcji możesz szpiegować zarówno metody nieeksportowanych obiektów, jak i nieeksportowane funkcje najwyższego poziomu, w następujący sposób:

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

Następnie możesz ustawić takie oczekiwania:

expect(dbLoader.rewired.fs.readFileSync).toHaveBeenCalled();
expect(dbLoader.rewired.message).toHaveBeenCalledWith(POST_DESCRIPTION);
Franco
źródło
0

możesz stworzyć nowy kontekst używając modułu vm i ewaluować znajdujący się w nim plik js, podobnie jak robi to repl. wtedy masz dostęp do wszystkiego, co zadeklaruje.

srosh
źródło
0

Znalazłem dość prosty sposób, który pozwala testować, szpiegować i mockować te wewnętrzne funkcje z poziomu testów:

Powiedzmy, że mamy taki moduł węzła:

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

function myInternalFn() {

}

function myExportableFn() {
    myInternalFn();   
}

exports.myExportableFn = myExportableFn;

Jeśli teraz chcesz przetestować i szpiega i mock myInternalFn natomiast nie eksportując go w produkcji musimy poprawić plik tak:

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;

Teraz możesz testować, szpiegować i wyszydzać myInternalFnwszędzie, gdzie go używasz, ponieważ testable.myInternalFnw produkcji nie jest eksportowany .

heinob
źródło
0

Nie jest to zalecana praktyka, ale jeśli nie możesz użyć rewirezgodnie z sugestią @Antoine, zawsze możesz po prostu przeczytać plik i użyć eval().

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

Znalazłem to przydatne podczas testowania jednostkowego plików JS po stronie klienta dla starszego systemu.

Pliki JS utworzyłyby wiele zmiennych globalnych windowbez żadnych instrukcji require(...)i module.exports(i tak nie było żadnego pakietu modułów, takiego jak Webpack lub Browserify, który mógłby usunąć te instrukcje).

Zamiast refaktoryzować całą bazę kodu, pozwoliło nam to zintegrować testy jednostkowe w naszym JS po stronie klienta.

Abhishek Divekar
źródło