- Posted by brunofig on December 19, 2007
At work I do a lot of javascript. The .net framework we develop has a extensive javascript library for the web controls to use and abuse. What happens is that many times we end up building pages to test parts of our js framework.
But you, the reader, must be thinking..."Why don't yo use a unit testing framework like JSUnit?"
Well this is one possible way... but what it would be nice was to have a way of testing the javascript in the same way we test the framework: in the build process.
The problem is that many of the javascript we do was one BIG dependency: the Browser. The window, the document, and other objects supplied by the browser are needed.
So Paulo suggested: "Why don't you build a Mock framework for javascript?"
This is a great ideia.... So I started coding (with the mock expert guidance of Paulo) and ended up with a way of mocking javascript. This is still in a early stage of development, but I wanted to show you what I'm doing hoping to get some feedback.
So, taking a simple javascript like the one bellow and build a mock test for it:
function GreetUser()
{
var txt = document.getElementById("txtName");
var lbl = document.getElementById("lblGreet");
lbl.innerHTML = "Hi " + txt.value;
}
Since we don't have the browser as the running environment, we need to mock the document object and his methods, that in this case is the getElmentById. The mocked document.getElmentById must then return an object containing the value property, in the case of the txtName, and the innerHTML for the lblGreet.
The first thing we must do is create a new instance of the JSMock object:
var mock = new JSMock();
Then we mock the document object like so:
var mockedDocument = mock.mockObject("document");
After we need to asign the new mocked object to the script container, this way simulating the browser enviroment:
this.document=mockedDocument.target;
This done, we must create the expectations for the mocked object, first for the txtName control:
var txt = { "value" : "Bruno Figueiredo" };
mockedDocument.expectAndReturn("getElementById", ["txtName"], txt );
Then for the lblGreet control:
var lbl = { "innerHTML" : "" };
mockedDocument.expectAndReturn("getElementById", ["lblGreet"], lbl );
All we need to the next is call the GreetUser function and assert the results:
GreetUser();
assert.AreEqual(lbl.innerHTML, "Hi Bruno Figueiredo");
Now, if I'm not using the browser to run the scripts, we need to use the Windows Script Host to run them. I don't know much about WSH so, this is a hudge problem for me. I've started reading about it and came up with this script to run the tests:
<job id="Job1">
<script language="JavaScript" src="JSUnit.js">
</script>
<script language="JavaScript" src="JSMock.js">
</script>
<script language="JavaScript">
var mock = new JSMock();
function GreetUser()
{
var txt = document.getElementById("txtName");
var lbl = document.getElementById("lblGreet");
lbl.innerHTML = "Hi " + txt.value;
}
function testGreetUser()
{
var mockedDocument = mock.mockObject("document");
this.document=mockedDocument.target;
var txt = { "value" : "Bruno Figueiredo" };
mockedDocument.expectAndReturn("getElementById", ["txtName"], txt );
var lbl = { "innerHTML" : "" };
mockedDocument.expectAndReturn("getElementById", ["lblGreet"], lbl );
GreetUser();
assert.AreEqual(lbl.innerHTML, "Hi Bruno Figueiredo");
}
JSUnit.container = this;
JSUnit.addTest("testGreetUser");
JSUnit.runAll();
</script>
</job>
To run just drop the above code in a file, for example test.swf and execute "cscript test.swf" on the command-line.
What happens is that I now need a way to capture the output somehow. Maybe you can help me out.
In the meanwhile i'll dig some more on the WSH and as soon as I have some new code I'll update this post.
Appendix
Code for the JSMock object:
var JSMock = function JSMock()
{
var mockedObjects = {};
this.mockObject = function JSMock$MockObject(name, returnType)
{
return this._createObject(name);
};
this._createObject = function JSMock$_createObject(objectName)
{
var mock = this;
return mockedObjects[objectName] = {
"target":{},
"expectAndReturn": function expectAndReturn(funcName, comparerValue, objBody)
{
var me = this;
var obj;
if (this.target[funcName] == null)
{
obj = {
"comparers" : {},
"func": function func()
{
return this.comparers[mock._serialize(arguments)];
}
};
}
else
{
obj = this.target[funcName].object;
}
obj.comparers[mock._serialize(comparerValue)] = objBody;
var returns = function(){
return obj.func.apply(obj, arguments);
};
this.target[funcName] = returns;
this.target[funcName].object = obj;
}
};
};
this._serialize = function JSMock$_serialize(arrayValue)
{
var serial = "";
for (i=0;i<arrayValue.length;i++)
{
serial+= new String(arrayValue[i]);
}
return serial;
};
}
Code for the JSUnit object:
var JSUnit = {
tests:null,
container:null,
runAll:function JSUnit$runAll(container)
{
container = container || this.container;
if (this.tests==null)
{
this.tests={};
for(var name in container)
{
if(name.indexOf("test")===0)
{
this.tests[name] = container[name];
}
}
}
for(var name in this.tests)
{
this.tests[name].apply(container);
}
},
run: function JSUnit$run(test)
{
test();
},
addTest: function JSUnit$addTest(testName)
{
if (this.tests==null)
{
this.tests={};
}
var test = this.tests[testName];
if (test == null || test == undefined)
{
this.tests[testName] = this.container[testName];
}
}
};
var assert = {
AreEqual: function Assert$isEqual(value1, value2, errorMessage)
{
var aux = (value1===value2);
if (aux) return true;
else
{
if (errorMessage)
{
throw errorMessage;
}
else
{
var msg = "The values aren't equal!\nExpected: " + value1 + "\nReceived: " + value2;
WScript.StdOut.Write(msg);
//WScript.Echo(msg);
}
return false;
}
}
};