Bagaimana cara mengakses dan menguji fungsi internal (non-ekspor) dalam modul node.js?

181

Saya mencoba mencari cara untuk menguji fungsi internal (yaitu tidak diekspor) di nodejs (lebih disukai dengan moka atau melati). Dan saya tidak tahu!

Katakanlah saya punya modul seperti itu:

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

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

exports.exported = exported;

Dan tes berikut (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));
    });
  });
});

Apakah ada cara untuk menguji unit notExportedfungsi tanpa benar-benar mengekspornya karena itu tidak dimaksudkan untuk diekspos?

xavier.seignard
sumber
1
Mungkin hanya memaparkan fungsi untuk menguji ketika di lingkungan tertentu? Saya tidak tahu prosedur standar di sini.
loganfsmyth

Jawaban:

243

The rewire Modul pasti jawabannya.

Ini kode saya untuk mengakses fungsi yang tidak diekspor dan mengujinya menggunakan 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
sumber
2
Ini benar-benar harus menjadi jawaban teratas. Itu tidak memerlukan penulisan ulang semua modul yang ada dengan ekspor spesifik NODE_ENV, juga tidak melibatkan membaca dalam modul sebagai teks.
Adam Yost
Solusi yang bagus. Mungkin untuk melangkah lebih jauh dan mengintegrasikannya dengan mata-mata dalam kerangka uji Anda. Bekerja dengan Jasmine, saya mencoba strategi ini .
Franco
2
Solusi bagus Apakah ada versi yang berfungsi untuk orang tipe Babel?
Charles Merriam
2
Menggunakan rewire dengan bercanda dan ts-bercanda (naskah) saya mendapatkan error berikut: Cannot find module '../../package' from 'node.js'. Pernahkah kamu melihat ini?
clu
2
Rewire memiliki masalah kompatibilitas dengan lelucon. Jest tidak akan mempertimbangkan fungsi yang dipanggil dari rewire dalam laporan cakupan. Itu agak mengalahkan tujuannya.
robross0606
10

Caranya adalah dengan mengatur NODE_ENVvariabel lingkungan ke sesuatu seperti testdan kemudian mengekspornya secara kondisional.

Dengan asumsi Anda belum menginstal moka secara global, Anda dapat memiliki Makefile di root direktori aplikasi Anda yang berisi yang berikut ini:

REPORTER = dot

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

.PHONY: test

Ini membuat file mengatur NODE_ENV sebelum menjalankan moka. Anda kemudian dapat menjalankan tes moka dengan make testdi baris perintah.

Sekarang, Anda dapat mengekspor secara kondisional fungsi Anda yang biasanya tidak diekspor hanya ketika tes mocha Anda berjalan:

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;

Jawaban lain menyarankan menggunakan modul vm untuk mengevaluasi file, tetapi ini tidak berhasil dan menimbulkan kesalahan yang menyatakan bahwa ekspor tidak didefinisikan.

Matthew Bradley
sumber
8
Ini seperti hack, apakah benar-benar tidak ada cara untuk menguji fungsi internal (non-ekspor) tanpa melakukan itu jika NODE_ENV memblokir?
RyanHirsch
2
Itu sangat jahat. Ini bukan cara terbaik untuk menyelesaikan masalah ini.
npiv
7

EDIT:

Memuat modul menggunakan vmdapat menyebabkan perilaku yang tidak terduga (misalnya instanceofoperator tidak lagi bekerja dengan objek yang dibuat dalam modul seperti itu karena prototipe global berbeda dari yang digunakan dalam modul yang dimuat secara normal dengan require). Saya tidak lagi menggunakan teknik di bawah ini dan sebagai gantinya menggunakan modul rewire . Ini bekerja dengan sangat baik. Inilah jawaban asli saya:

Menguraikan jawaban srosh ...

Rasanya agak gila, tapi saya menulis modul "test_utils.js" sederhana yang akan memungkinkan Anda melakukan apa yang Anda inginkan tanpa memiliki ekspor bersyarat dalam modul aplikasi Anda:

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

Ada beberapa hal lagi yang termasuk dalam moduleobjek gobal modul simpul yang mungkin juga perlu masuk ke contextobjek di atas, tetapi ini adalah set minimum yang saya butuhkan agar bisa berfungsi.

Berikut ini contoh menggunakan 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
sumber
2
dapatkah Anda memberi contoh bagaimana Anda mengakses fungsi yang tidak diekspor menggunakan rewire?
Matthias
1
Hai Matthias, saya telah memberi Anda sebuah contoh melakukan hal itu dalam jawaban saya. Jika Anda suka, mungkin saya mengajukan beberapa pertanyaan? :) Hampir semua pertanyaan saya ada pada 0 dan StackOverflow berpikir untuk membekukan pertanyaan saya. X_X
Anthony
2

Bekerja dengan Jasmine, saya mencoba masuk lebih dalam dengan solusi yang diusulkan oleh Anthony Mayfield , berdasarkan rewire .

Saya menerapkan fungsi berikut ( Perhatian : belum diuji secara menyeluruh, hanya dibagikan sebagai strategi yang memungkinkan) :

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

Dengan fungsi seperti ini, Anda dapat memata-matai kedua metode objek yang tidak diekspor dan fungsi tingkat atas yang tidak diekspor, sebagai berikut:

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

Kemudian Anda dapat menetapkan harapan seperti ini:

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

Anda dapat membuat konteks baru menggunakan modul vm dan mengevaluasi file js di dalamnya, semacam seperti repl. maka Anda memiliki akses ke semua yang dinyatakannya.

srosh
sumber
0

Saya telah menemukan cara yang cukup sederhana yang memungkinkan Anda untuk menguji, memata-matai dan mengejek fungsi-fungsi internal dari dalam tes:

Katakanlah kita memiliki modul simpul seperti ini:

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

function myInternalFn() {

}

function myExportableFn() {
    myInternalFn();   
}

exports.myExportableFn = myExportableFn;

Jika sekarang kita ingin menguji dan memata - matai dan mengejek myInternalFn tanpa mengekspornya dalam produksi, kita harus memperbaiki file seperti ini:

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;

Sekarang Anda dapat menguji, memata-matai dan mengejek di myInternalFnmana - mana di mana Anda menggunakannya testable.myInternalFndan dalam produksi tidak diekspor .

heinob
sumber
0

Ini bukan praktik yang disarankan, tetapi jika Anda tidak dapat menggunakan rewireseperti yang disarankan oleh @Antoine, Anda selalu dapat hanya membaca file dan menggunakan eval().

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

Saya menemukan ini berguna saat unit menguji file JS sisi klien untuk sistem lawas.

File JS akan mengatur banyak variabel global windowtanpa ada require(...)danmodule.exports pernyataan (tidak ada bundler modul seperti Webpack atau Browserify yang tersedia untuk menghapus pernyataan ini pula).

Daripada memperbaiki seluruh basis kode, ini memungkinkan kami untuk mengintegrasikan tes unit di JS sisi klien kami.

Abhishek Divekar
sumber