When running unit tests in JavaScript it is often useful to gain access to private members. Previously I would add an extra snippet of code inside the class definition (or whatever one calls this in a prototype-based language) to open up the object. Something like this was usually sufficient:
this.internal = function(name) {
return eval(name);
}
(And before anyone comments of the evils of eval, keep in mind that I'm using only this for testing purposes and it is not included in the final deployment file - I use a grunt script and some regexp to automatically remove it).
This (above) has been working just fine, but I had a niggling feeling that a better (and more general) solution should be possible. I was doing some other work on code instrumentation in the browser and realized that this was a good candidate for some real-time code instrumentation. So, I created this simple functionality for adding some extra functions to a class that allows access to first-level private members:
var CodeInspector = new function() {
var prefixPattern = /^[a-zA-Z_$][0-9a-zA-Z_$]*/;
this.instrumentClass = function(classObject, prefix) {
if(Object.prototype.toString.call(classObject) !== "[object Function]") {
return false;
}
if(prefix === undefined || prefix === null) {
prefix = "";
}
else if(!prefixPattern.test(prefix)) {
return false;
}
var internalCode = " \n\
this."+prefix+"getPrivate = function(name) { \n\
return eval(name); \n\
} \n\
this."+prefix+"setPrivate = function(name, value) { \n\
eval(name+' = value'); \n\
} \n\
this."+prefix+"callPrivate = function(name) { \n\
return eval(name+'(arguments)'); \n\
} \n\
";
var code, indexClosingBrace;
code = classObject.toString();
indexClosingBrace = code.lastIndexOf("}");
code = code.substring(0, indexClosingBrace);
code += "\n" + internalCode + "\n}";
return eval("[" + code + "]")[0];
};
}();
To instrument a class you do this:
MyClass = CodeInspector.instrumentClass(MyClass);
This will replace the class definition with the instrumented version, which has three new methods to read/get (getPrivate
), write/set private members (setPrivate
) and to call private functions (callPrivate
). Strictly speaking, callPrivate
is not necessary since you can always get a reference to the private method via getPrivate
and then call it from outside since closure will keep it "inside" the class definition.
I've used it together with QUnit and the Blanket.js code coverage utility and I was pleasantly surprised to find that it didn't mess up the code coverage function.
Here is an example of QUnit test code:
var testClassObject;
var prefix = "__INSTRUMENTED__";
QUnit.begin(function(details) {
TestClass = CodeInspector.instrumentClass(TestClass, prefix);
testClassObject = new TestClass();
});
QUnit.test("getPrivate - get value", function(assert) {
expect(1);
var privateValue = testClassObject[prefix+"getPrivate"]("_privateValue");
assert.ok(privateValue, "Value returned");
});
... and this is the simple test class I'm testing:
function TestClass() {
var _privateValue = "A";
function _privateFunction() {
return "B";
}
function NestledClass() {
var _nestledPrivateValue;
}
}
Note that it only gives access to first level private members. No access is gained to private members of nestled classes. It would probably be possible to instrument second-level classes using regular expressions to decompose the code at text level, but so far I haven't had a need to go beyond the first level.
I'm looking for feedback on the idea, structure and implementation. This is just a quick first attempt. It is working and I've used it successfully to replace my old version with the added code snippet in a couple of projects, but I would like some feedback on possible issues that could restrict its generality, specifically the way the methods are injected before the ending brace.
Are there alternative ways of coding a constructor function that need to be checked for? At the moment I'm only using it to test my own classes, but I would like to make it into a utility that could be used to open up any class. Also, if there are potential cross-browser issues. And, finally, any other feedback or tips on changes, improvements and extensions.