How to access and test an internal (non-exports) function in a node.js module?

后端 未结 8 1876
别跟我提以往
别跟我提以往 2020-12-12 09:31

I\'m trying to figure out on how to test internal (i.e. not exported) functions in nodejs (preferably with mocha or jasmine). And i have no idea!

Let say I have a mo

相关标签:
8条回答
  • 2020-12-12 09:51

    Essentially you need to merge the source context with the test cases - one way to do this would be using a small helper function wrapping the tests.

    demo.js

    const internalVar = 1;
    
    

    demo.test.js

    const importing = (sourceFile, tests) => eval(`${require('fs').readFileSync(sourceFile)};(${String(tests)})();`);
    
    
    importing('./demo.js', () => {
        it('should have context access', () => {
            expect(internalVar).toBe(1);
        });
    });
    
    
    0 讨论(0)
  • 2020-12-12 09:54

    The rewire module is definitely the answer.

    Here's my code for accessing an unexported function and testing it using 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');
    
    
    var 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();
      });
    });
    
    0 讨论(0)
  • 2020-12-12 09:59

    you can make a new context using vm module and eval the js file in it, sort of like repl does. then you have access to everything it declares.

    0 讨论(0)
  • 2020-12-12 10:00

    I have found a quite simple way that allows you to test, spy and mock those internal functions from within the tests:

    Let's say we have a node module like this:

    mymodule.js:
    ------------
    "use strict";
    
    function myInternalFn() {
    
    }
    
    function myExportableFn() {
        myInternalFn();   
    }
    
    exports.myExportableFn = myExportableFn;
    

    If we now want to test and spy and mock myInternalFn while not exporting it in production we have to improve the file like this:

    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;
    

    Now you can test, spy and mock myInternalFn everywhere where you use it as testable.myInternalFn and in production it is not exported.

    0 讨论(0)
  • 2020-12-12 10:08

    This is not recommended practice, but if you can't use rewire as suggested by @Antoine, you can always just read the file and use eval().

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

    I found this useful while unit testing client-side JS files for a legacy system.

    The JS files would set up a lot of global variables under window without any require(...) and module.exports statements (there was no module bundler like Webpack or Browserify available to remove these statements anyway).

    Rather than refactor the entire codebase, this allowed us to integrate unit tests in our client-side JS.

    0 讨论(0)
  • 2020-12-12 10:12

    Working with Jasmine, I tried to go deeper with the solution proposed by Anthony Mayfield, based on rewire.

    I implemented the following function (Caution: not yet thoroughly tested, just shared as a possibile strategy):

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

    With a function like this you could spy on both methods of non-exported objects and non-exported top level functions, as follows:

    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
    

    Then you can set expectations like these:

    expect(dbLoader.rewired.fs.readFileSync).toHaveBeenCalled();
    expect(dbLoader.rewired.message).toHaveBeenCalledWith(POST_DESCRIPTION);
    
    0 讨论(0)
提交回复
热议问题