If you've ever written tests for a Node. js application, chances are you used an external library. However, you don't need a library to run unit tests in Javascript.
The assert module. The basis for most Node unit testing is the built-in assert module, which tests a condition and, if the condition isn't met, throws an error. Node's assert module is used by many third-party testing frameworks. Even without a testing framework, you can do useful testing with it.
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();
});
});
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.
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));
});
});
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);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With