As our web applications become more JavaScript heavy it is becomes increasingly important that we have tests for this code. In the past I have managed to get away with it but in retrospect it was a risky approach to take. So in my latest project I started assessing the various JavaScript unit testing frameworks and decided Scriptaculous was the way to go. I excluded JsUnit as an option because it is not object oriented and didn't give me enough information about which tests had passed. The Scriptaculous unit testing framework also has some useful features like the ability to wait before completing the test and benchmarking of calls.
The one big problem you have when testing an AJAX application however is that the out-of-band calls are made asynchronously. This means that if you are testing a method that does a load and want to ensure that certain values are set or calls made once the data has loaded you have to have a bit of a problem. The Scriptaculous framework's solution to this is the ability to wait and execute a function that may then contain your asserts. Here is a basic example of what the test would look like:
I found that this led to inconsistent test results because for various reasons the webservice request callbacks would take a varying amount of time. Of course you could try to cover yourself by making the wait time larger but then your tests take too long to run. Both of these problems will discourage others from using and extending your tests.
Having hand coded XMLHTTP calls in the days before the approach was known as AJAX I knew that it was possible to make the calls synchronously, so all I needed to do was get the generated ASP.Net AJAX webservice proxies to use a new synchronous executor. I came across Amit's SJAX post that demonstrated how to created a synchronous executor for a Sys.Net.WebRequest object which was very useful. I discovered that one could change the executor for generated webservice proxy calls by changing the default executor type for the Sys.Net.WebRequestManager, like so:
So I took Amit's example, changed it somewhat and got it to work work as a synchronous request handler (using the debug version of the ASP.Net AJAX framework JavaScript as a guide). This resulted in a handler that looked like this (note I have removed code and replaced them with comments for brevity) [download full file here]:
So now we can create tests that test objects that make synchronous webservice calls by changing the default executor in the setup of the unit test like this:
I think you'll agree that with the extensibility they have provided in the ASP.Net AJAX framework, what we have now is quite a neat way of ensuring that our tests run consistently and as fast as they can. My thanks go out to Microsoft and the Scriptaculous team! :)Image may be NSFW.
Clik here to view.
The one big problem you have when testing an AJAX application however is that the out-of-band calls are made asynchronously. This means that if you are testing a method that does a load and want to ensure that certain values are set or calls made once the data has loaded you have to have a bit of a problem. The Scriptaculous framework's solution to this is the ability to wait and execute a function that may then contain your asserts. Here is a basic example of what the test would look like:
new Test.Unit.Runner(
{
testShouldLoadDataOnInit: function()
{
var manager = new TestManager();
//This makes a web service call
manager.onInit();
this.wait(500, function()
{
this.assertNotEqual(manager._TestData, null);
});
},
});
{
testShouldLoadDataOnInit: function()
{
var manager = new TestManager();
//This makes a web service call
manager.onInit();
this.wait(500, function()
{
this.assertNotEqual(manager._TestData, null);
});
},
});
I found that this led to inconsistent test results because for various reasons the webservice request callbacks would take a varying amount of time. Of course you could try to cover yourself by making the wait time larger but then your tests take too long to run. Both of these problems will discourage others from using and extending your tests.
Having hand coded XMLHTTP calls in the days before the approach was known as AJAX I knew that it was possible to make the calls synchronously, so all I needed to do was get the generated ASP.Net AJAX webservice proxies to use a new synchronous executor. I came across Amit's SJAX post that demonstrated how to created a synchronous executor for a Sys.Net.WebRequest object which was very useful. I discovered that one could change the executor for generated webservice proxy calls by changing the default executor type for the Sys.Net.WebRequestManager, like so:
Sys.Net.WebRequestManager.set_defaultExecutorType("Example.Executor");
So I took Amit's example, changed it somewhat and got it to work work as a synchronous request handler (using the debug version of the ASP.Net AJAX framework JavaScript as a guide). This resulted in a handler that looked like this (note I have removed code and replaced them with comments for brevity) [download full file here]:
Type.registerNamespace('Sjax');
Sjax.XMLHttpSyncExecutor = function()
{
Sjax.XMLHttpSyncExecutor.initializeBase(this);
this._started = false;
this._responseAvailable = false;
this._onReceiveHandler = null;
this._xmlHttpRequest = null;
this.get_aborted = function()
{
//Parameter validation code removed here...
return false;
}
this.get_responseAvailable = function()
{
//Parameter validation code removed here...
return this._responseAvailable;
}
this.get_responseData = function()
{
//Parameter validation code removed here...
return this._xmlHttpRequest.responseText;
}
this.get_started = function()
{
//Parameter validation code removed here...
return this._started;
}
this.get_statusCode = function()
{
//Parameter validation code removed here...
return this._xmlHttpRequest.status;
}
this.get_statusText = function()
{
//Parameter validation code removed here...
return this._xmlHttpRequest.statusText;
}
this.get_xml = function()
{
//Code removed
}
this.executeRequest = function()
{
//Parameter validation code removed here...
var webRequest = this.get_webRequest();
if (webRequest === null)
{
throw Error.invalidOperation(Sys.Res.nullWebRequest);
}
var body = webRequest.get_body();
var headers = webRequest.get_headers();
var verb = webRequest.get_httpVerb();
var xmlHttpRequest = new XMLHttpRequest();
this._onReceiveHandler = Function.createCallback(this._onReadyStateChange, { sender:this });
this._started = true;
xmlHttpRequest.onreadystatechange = this._onReceiveHandler;
xmlHttpRequest.open(verb, webRequest.getResolvedUrl(), false); // False to call Synchronously
if (headers)
{
for (var header in headers)
{
var val = headers[header];
if (typeof(val) !== "function")
{
xmlHttpRequest.setRequestHeader(header, val);
}
}
}
if (verb.toLowerCase() === "post")
{
if ((headers === null) || !headers['Content-Type'])
{
xmlHttpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
}
if (!body)
{
body = '';
}
}
this._started = true;
this._xmlHttpRequest = xmlHttpRequest;
xmlHttpRequest.send(body);
}
this.getAllResponseHeaders = function()
{
//Parameter validation code removed here...
return this._xmlHttpRequest.getAllResponseHeaders();
}
this.getResponseHeader = function(header)
{
//Parameter validation code removed here...
return this._xmlHttpRequest.getResponseHeader(header);
}
this._onReadyStateChange = function(e)
{
var executor = e.sender;
if (executor._xmlHttpRequest.readyState === 4)
{
//Validation code removed here...
executor._responseAvailable = true;
executor._xmlHttpRequest.onreadystatechange = Function.emptyMethod;
executor._onReceiveHandler = null;
executor._started = false;
var webRequest = executor.get_webRequest();
webRequest.completed(Sys.EventArgs.Empty);
//Once the completed callback handler has processed the data it needs from the XML HTTP request we can clean up
executor._xmlHttpRequest = null;
}
}
}
Sjax.XMLHttpSyncExecutor.registerClass('Sjax.XMLHttpSyncExecutor', Sys.Net.WebRequestExecutor);
Sjax.XMLHttpSyncExecutor = function()
{
Sjax.XMLHttpSyncExecutor.initializeBase(this);
this._started = false;
this._responseAvailable = false;
this._onReceiveHandler = null;
this._xmlHttpRequest = null;
this.get_aborted = function()
{
//Parameter validation code removed here...
return false;
}
this.get_responseAvailable = function()
{
//Parameter validation code removed here...
return this._responseAvailable;
}
this.get_responseData = function()
{
//Parameter validation code removed here...
return this._xmlHttpRequest.responseText;
}
this.get_started = function()
{
//Parameter validation code removed here...
return this._started;
}
this.get_statusCode = function()
{
//Parameter validation code removed here...
return this._xmlHttpRequest.status;
}
this.get_statusText = function()
{
//Parameter validation code removed here...
return this._xmlHttpRequest.statusText;
}
this.get_xml = function()
{
//Code removed
}
this.executeRequest = function()
{
//Parameter validation code removed here...
var webRequest = this.get_webRequest();
if (webRequest === null)
{
throw Error.invalidOperation(Sys.Res.nullWebRequest);
}
var body = webRequest.get_body();
var headers = webRequest.get_headers();
var verb = webRequest.get_httpVerb();
var xmlHttpRequest = new XMLHttpRequest();
this._onReceiveHandler = Function.createCallback(this._onReadyStateChange, { sender:this });
this._started = true;
xmlHttpRequest.onreadystatechange = this._onReceiveHandler;
xmlHttpRequest.open(verb, webRequest.getResolvedUrl(), false); // False to call Synchronously
if (headers)
{
for (var header in headers)
{
var val = headers[header];
if (typeof(val) !== "function")
{
xmlHttpRequest.setRequestHeader(header, val);
}
}
}
if (verb.toLowerCase() === "post")
{
if ((headers === null) || !headers['Content-Type'])
{
xmlHttpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
}
if (!body)
{
body = '';
}
}
this._started = true;
this._xmlHttpRequest = xmlHttpRequest;
xmlHttpRequest.send(body);
}
this.getAllResponseHeaders = function()
{
//Parameter validation code removed here...
return this._xmlHttpRequest.getAllResponseHeaders();
}
this.getResponseHeader = function(header)
{
//Parameter validation code removed here...
return this._xmlHttpRequest.getResponseHeader(header);
}
this._onReadyStateChange = function(e)
{
var executor = e.sender;
if (executor._xmlHttpRequest.readyState === 4)
{
//Validation code removed here...
executor._responseAvailable = true;
executor._xmlHttpRequest.onreadystatechange = Function.emptyMethod;
executor._onReceiveHandler = null;
executor._started = false;
var webRequest = executor.get_webRequest();
webRequest.completed(Sys.EventArgs.Empty);
//Once the completed callback handler has processed the data it needs from the XML HTTP request we can clean up
executor._xmlHttpRequest = null;
}
}
}
Sjax.XMLHttpSyncExecutor.registerClass('Sjax.XMLHttpSyncExecutor', Sys.Net.WebRequestExecutor);
So now we can create tests that test objects that make synchronous webservice calls by changing the default executor in the setup of the unit test like this:
new Test.Unit.Runner(
{
setup: function()
{
Sys.Net.WebRequestManager.set_defaultExecutorType("Sjax.XMLHttpSyncExecutor");
},
testShouldLoadDataOnInit: function()
{
var manager = new TestManager();
manager.onInit();
this.assertNotEqual(manager._TestData, null);
},
});
{
setup: function()
{
Sys.Net.WebRequestManager.set_defaultExecutorType("Sjax.XMLHttpSyncExecutor");
},
testShouldLoadDataOnInit: function()
{
var manager = new TestManager();
manager.onInit();
this.assertNotEqual(manager._TestData, null);
},
});
I think you'll agree that with the extensibility they have provided in the ASP.Net AJAX framework, what we have now is quite a neat way of ensuring that our tests run consistently and as fast as they can. My thanks go out to Microsoft and the Scriptaculous team! :)Image may be NSFW.
Clik here to view.