235

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 module like that:

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

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

exports.exported = exported;

And the following test (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));
    });
  });
});

Is there any way to unit test the notExported function without actually exporting it since it's not meant to be exposed?

Chris Seymour
  • 83,387
  • 30
  • 160
  • 202
xavier.seignard
  • 11,254
  • 13
  • 51
  • 75
  • 1
    Maybe just expose the functions to test when in a specific environment? I don't know standard procedure here. – loganfsmyth Feb 14 '13 at 17:01
  • 5
    There is a reason why it's not exported. Test the public interface only, anything private will be tested along the way. – Alejandro Sep 07 '20 at 14:44
  • 3
    True, but then it is more like integration test than unit test if you test only `exported` and let that drive the testing of `notExported`. Additionally, this integration approach makes it difficult/impossible to test how `exported` reacts to failures in `notExported`, since you can't access `notExported` from your unit test in order to drive it to failure. – Mawg says reinstate Monica Feb 27 '22 at 12:24

11 Answers11

297

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();
  });
});
Tom Smykowski
  • 25,487
  • 54
  • 159
  • 236
Anthony
  • 13,434
  • 14
  • 60
  • 80
  • 2
    This should absolutely be the top answer. It does not require rewriting all existing modules with NODE_ENV specific exports, nor does it involve reading in the module as text. – Adam Yost Jul 22 '15 at 16:00
  • Beutiful solution. It is possibile to go further and integrate it with spies in your test framework. Working with Jasmine, I tried [this strategy](http://stackoverflow.com/a/39818726/659788). – Franco Oct 02 '16 at 17:08
  • 3
    Great solution. Is there a working version for Babel type people? – Charles Merriam May 05 '17 at 21:42
  • 3
    Using rewire with jest and ts-jest (typescript) I get the following error: `Cannot find module '../../package' from 'node.js'`. Have you seen this? – b.lyte Nov 28 '17 at 07:48
  • 6
    Rewire has a compatibility issue with jest. Jest will not consider functions called from rewire in coverage reports. That somewhat defeats the purpose. – robross0606 Nov 04 '19 at 14:54
  • Yes I also had the `Cannot find module` issue. For my case it happens when using `jest.resetModules()`. In this case it seems like rewire path needed to be relative to the CWD rather than relative path from test to module – Andre Miras Dec 06 '19 at 16:22
  • 2
    Yeah, it's *a solution* . The only problem is that rewired modules are not taken into account in the test coverage report in Jest. – Tomasz Kasperczyk Jan 14 '20 at 16:04
  • Is there any way to use rewire with rollupjs? – Chanuka Asanka Sep 28 '20 at 04:43
  • rewire depends on nodejs native apis like `fs` and `module`...so isn't a solution for exposing non-exported functions outside of node (e.g. when running unit tests in browser via Karma)...need conditional exports I guess in that case. There is `babel-plugin-rewire` which works for ESM (not commonjs) that might work for browser stuff. – mattpr Jan 16 '23 at 11:31
18

The trick is to set the NODE_ENV environment variable to something like test and then conditionally export it.

Assuming you've not globally installed mocha, you could have a Makefile in the root of your app directory that contains the following:

REPORTER = dot

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

.PHONY: test

This make file sets up the NODE_ENV before running mocha. You can then run your mocha tests with make test at the command line.

Now, you can conditionally export your function that isn't usually exported only when your mocha tests are running:

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;

The other answer suggested using a vm module to evaluate the file, but this doesn't work and throws an error stating that exports is not defined.

Matthew Bradley
  • 441
  • 3
  • 7
  • 15
    This seems like a hack, is there really no way to test internal (non-exported) functions without doing a that if NODE_ENV block? – RyanHirsch Jan 15 '14 at 16:47
  • 8
    That's pretty nasty. This can't be the best way to solve this problem. – npiv Oct 23 '14 at 20:45
  • I don't think is a hack at all -- if you are able to modify the lib then this is a completely stock way to export unexported identifiers without any third-party dependencies. At the end of the day both allow you to access things that you aren't supposed to be able to normally access -- so by that logic this whole idea is a hack and nasty. – prasanthv Jan 11 '21 at 03:56
  • I wish Stack Overflow had an option to bookmark answers. –  Nov 18 '22 at 18:55
  • For server side code, rewire works pretty well. But if you want to do in-browser unit testing (e.g. running your mocha tests with karma) then rewire won't work. The conditional export based on test environment seems to be the most universally compatible approach. – mattpr Mar 02 '23 at 11:29
8

EDIT:

Loading a module using vm can cause unexpected behavior (e.g. the instanceof operator no longer works with objects that are created in such a module because the global prototypes are different from those used in module loaded normally with require). I no longer use the below technique and instead use the rewire module. It works wonderfully. Here's my original answer:

Elaborating on srosh's answer...

It feels a bit hacky, but I wrote a simple "test_utils.js" module that should allow you to do what you want without having conditional exports in your application modules:

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

There are some more things that are included in a node module's gobal module object that might also need to go into the context object above, but this is the minimum set that I need for it to work.

Here's an example using 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
  • 1,364
  • 14
  • 12
4

I have been using a different approach, without any dependencies: Have a __testing export with all the local functions I want to test, which value depends on NODE_ENV, so it's only accessible on tests:

// file.ts
const localFunction = () => console.log('do something');
const localFunciton2 = () => console.log('do something else');
export const exportedFunction = () => {
    localFunction();
    localFunciton2();
}
export const __testing = (process.env.NODE_ENV === 'test') ? {
    localFunction, localFunction2
} : void 0;

// file.test.ts
import { __testing, exportedFunction } from './file,ts'
const { localFunction, localFunction2 } = __testing!;
// Now you can test local functions
2

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);
Community
  • 1
  • 1
Franco
  • 669
  • 2
  • 8
  • 23
1

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.

heinob
  • 19,127
  • 5
  • 41
  • 61
1

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

Ian Carter
  • 548
  • 8
  • 7
0

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.

srosh
  • 124
  • 1
  • 3
0

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.

Abhishek Divekar
  • 1,131
  • 2
  • 15
  • 31
0

eval doesn't really work on its own (it will only work with top-level function or var declarations), you can't capture top-level variables that are declared with let or const into the current context with eval, however, using a vm and running it in the current context will allow you to access all top-level variables after its execution...

eval("let local = 42;")
// local is undefined/undeclared here
const vm = require("vm")
vm.runInThisContext("let local = 42;");
// local is 42 here

...although declarations or assignments in the "imported" module could clash with anything already declared/defined in the current context by the time the vm starts if they share the same name.

Here's a mediocre solution. This will add a small bit of unnecessary code to your imported modules/units however, and your test suite would have to run each file directly in order to run its unit tests in this manner. Running your modules directly to do anything but its run unit tests would be out of question without even more code.

In the imported module, check if file is the main module, if so, run tests:

const local = {
  doMath() {return 2 + 2}
};

const local2 = 42;

if (require.main === module) {
  require("./test/tests-for-this-file.js")({local, local2});
} 

Then in the test file/module that imports the target module:

module.exports = function(localsObject) {
  // do tests with locals from target module
}

Now run your target module directly with node MODULEPATH to run its tests.

tmillr
  • 83
  • 1
  • 6
0

Perhaps try a conditional export like :

NOTE: This solution would enable the function being included in the test coverage report, as due to dependency issue of rewire with Jest, functions tested via rewire is not included in testing coverage, hence the purpose is defeated - @ robross0606

a.js

module.exports = {
  foo,
  foo1: process.env['NODE_DEV'] == 'TEST123' && foo1
}

a.test.js

process.env['NODE_DEV'] = 'TEST123'

const { foo1, foo } = require('./a')

This is would ensure that the tested function is also included in the test coverage.

Kushal Jaiswal
  • 103
  • 1
  • 5