In the pursuit of 100% code coverage, I am attempting to use mocha to test that my javascript module is loading correctly under AMD
, CommonJS/Node
, and browser
conditions. The pattern I'm using is below:
my-module.js
(function(global){
function MyClass(){}
// AMD
if(typeof define === 'function' && define.amd){
define(function(){
return MyClass;
});
// CommonJS/Node
} else if (typeof module !== 'undefined' && module.exports){
module.exports = MyClass;
// Browser
} else {
global.MyClass = MyClass;
}
})(this);
Since I am running my tests with node, define
is never defined, and module
is always defined; so the "CommonJS/Node" condition is the only one that ever gets tested.
What I have tried so far is something like this:
my-module.test.js
var MyClass = require('./my-module');
describe('MyClass', function(){
// suite of tests for the class itself
// uses 'var instance = new MyClass();' in each test
// all of these tests pass
});
describe('Exports', function(){
// suite of tests for the export portion
beforeEach(function(){
MyClass = null; // will reload module for each test
define = null; // set 'define' to null
module = null; // set 'module' to null
});
// tests for AMD
describe('AMD', function(){
it('should have loaded as AMD module', function(){
var define = function(){};
define.amd = true;
MyClass = require('./my-module'); // might be cached?
// hoping this reloads with 'define' in its parent scope
// but it does not. AMD condition is never reached.
expect(spy).to.have.been.called(); // chai spy, code omitted
});
});
});
I'm using spies to check if define
has been called, but the module doesn't show any signs of ever being reloaded with define
available to it. How can I achieve that?
And is there a safe way of nullifying module
so that I can test the browser condition as well?
In the pursuit of 100% code coverage, I am attempting to use mocha to test that my javascript module is loading correctly under AMD
, CommonJS/Node
, and browser
conditions. The pattern I'm using is below:
my-module.js
(function(global){
function MyClass(){}
// AMD
if(typeof define === 'function' && define.amd){
define(function(){
return MyClass;
});
// CommonJS/Node
} else if (typeof module !== 'undefined' && module.exports){
module.exports = MyClass;
// Browser
} else {
global.MyClass = MyClass;
}
})(this);
Since I am running my tests with node, define
is never defined, and module
is always defined; so the "CommonJS/Node" condition is the only one that ever gets tested.
What I have tried so far is something like this:
my-module.test.js
var MyClass = require('./my-module');
describe('MyClass', function(){
// suite of tests for the class itself
// uses 'var instance = new MyClass();' in each test
// all of these tests pass
});
describe('Exports', function(){
// suite of tests for the export portion
beforeEach(function(){
MyClass = null; // will reload module for each test
define = null; // set 'define' to null
module = null; // set 'module' to null
});
// tests for AMD
describe('AMD', function(){
it('should have loaded as AMD module', function(){
var define = function(){};
define.amd = true;
MyClass = require('./my-module'); // might be cached?
// hoping this reloads with 'define' in its parent scope
// but it does not. AMD condition is never reached.
expect(spy).to.have.been.called(); // chai spy, code omitted
});
});
});
I'm using spies to check if define
has been called, but the module doesn't show any signs of ever being reloaded with define
available to it. How can I achieve that?
And is there a safe way of nullifying module
so that I can test the browser condition as well?
3 Answers
Reset to default 3You might want to check out the rewire module. I'm not 100% sure, but I think it'll let you do what you need.
https://github./jhnns/rewire
I was able to create a custom solution (borrowing the majority of this code from http://howtonode/testing-private-state-and-mocking-deps)
module-loader.js
This module basically creates a new context in which your custom properties are available in the same global space as require
and console
, etc.
var vm = require('vm');
var fs = require('fs');
var path = require('path');
var extend = require('extend'); // install from npm
/**
* Helper for unit testing:
* - load module with mocked dependencies
* - allow accessing private state of the module
*
* @param {string} filePath Absolute path to module (file to load)
* @param {Object=} mocks Hash of mocked dependencies
*/
exports.loadModule = function(filePath, mocks) {
mocks = mocks || {};
// this is necessary to allow relative path modules within loaded file
// i.e. requiring ./some inside file /a/b.js needs to be resolved to /a/some
var resolveModule = function(module) {
if (module.charAt(0) !== '.') return module;
return path.resolve(path.dirname(filePath), module);
};
var exports = {};
var context = {
require: function(name) {
return mocks[name] || require(resolveModule(name));
},
console: console,
exports: exports,
module: {
exports: exports
}
};
var extendMe = {};
extend(true, extendMe, context, mocks);
// runs your module in a VM with a new context containing your mocks
// http://nodejs/api/vm.html#vm_vm_runinnewcontext_code_sandbox_filename
vm.runInNewContext(fs.readFileSync(filePath), extendMe);
return extendMe;
};
my-module.test.js
var loadModule = require('./module-loader').loadModule;
// ...
it('should load module with mocked global vars', function(){
function mockMethod(str){
console.log("mock: "+str);
}
var MyMockModule = loadModule('./my-module.js', {mock:mockMethod});
// 'MyClass' is available as MyMockModule.module.exports
});
my-module.js
(function(global){
function MyClass(){}
if(typeof mock !== 'undefined'){
mock("testing"); // will log "mock: testing"
}
module.exports = MyClass;
})(this);
Here's a lightweight solution that worked for me:
let document = (typeof document === "undefined") ? {} : document;
Unfortunately, this has to go in the file being tested, would be cumbersome for cases where more of document
's functionality is used, and doesn't help with needing test cases where something is undefined. But for simple cases, here's a simple solution.
(This is the answer I was looking for when I found this question)