To make a library ready for production, you need to write unit tests that cover all the core functionalities of the library. Then you will need to establish a means to automatically run those unit tests in different target environments.
Only when a library passes all unit tests in all target environments, it’s ready for production. In that regard, the unit tests of o2.js are still in progress and I’m completing the unit tests of one module after another.
In those tests, I try to cover all of the public methods of the tested objects.
Having been busy writing and executing those unit tests, here’s what I found:
Constructing and running unit tests enabled me to find out browser-specific bugs that I could have easily overlooked.
Unit Testing in Action
In former posts we’ve covered:
- The best practices of writing unit tests with JavaScript,
- How to make a unit test runner,
- How to actually write a test suite,
- And how to sequentially run a group of test suites together.
Before continuing any further, I strongly recommend you read those articles
.
Read them already? Then let us continue with our discussion
.
Let’s begin with the following test case extracted from the o2.ajax.js test suite:
add('o2.Ajax SHOULD return to oncomplete handler, after aborting a request', {
count: 2,
test: function() {
var me = this;
var url = 'service/service.php';
// this puts a 5-second sleep to service response.
var params = {wait : true};
var isDone = false;
var request = o2.Ajax.get(url, params, {
oncomplete: function() {
isDone = true;
assert(me, true, 'oncomplete fired.');
me.terminate();
}
});
setTimeout(function() {
request.abort();
}, 500);
setTimeout(function() {
assertStrictEqual(me, isDone, true, 'Request is timely processed.');
me.terminate();
}, 2000);
}
});
Where service.php is as follows:
<?php
/*
* <!--
* This program is distributed under
* the terms of the MIT license.
* Please see the LICENSE file for details.
* -->
*/
/**
*
*/
class Request {
/**
*
*/
public static function get_item($name) {
return isset($_POST[$name]) ? $_POST[$name] : (
isset($_GET[$name]) ? $_GET[$name] : null
);
}
}
/**
*
*/
class PageController {
/**
*
*/
public static function load_page() {
$options = Request::get_item('options');
$error = Request::get_item('error');
$echo = Request::get_item('echo');
$wait = Request::get_item('wait');
if($wait) {
sleep(5);
}
if ($options) {
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Max-Age: 1000');
header('Access-Control-Allow-Headers: *');
}
if ($error) {
header('HTTP/1.1 500 Internal Server Error');
return;
}
if ($echo) {
echo Request::get_item('data');
return;
}
echo '0';
}
}
PageController::load_page();
?>
When we run the test suite, the test case fails with the following output:
FAIL: Request is timely processed. FAIL: Completed: "o2.Ajax SHOULD return to oncomplete handler, after aboring a request": (success: 0 , failure: 1)
This means, the oncomplete handler of the AJAX get request never fires.
Let’s look at the appropriate section of o2.Ajax module that is responsible for emitting those events:
...
isSuccess = status === kOk || status === kCached;
callbacks = callbacks || {};
// Since the response has come, mark the request as "completed".
xhr.isComplete = true;
try {
if (isSuccess) {
oncomplete(responseText, responseXml, xhr);
return;
}
onerror(status, statusText, xhr);
} catch(ex) {
onexception(ex, xhr);
} finally {
finalizeXhr(xhr);
}
...
When we observe the above code section, the following finding is obvious:
If there’s an exception in evaluating the callback, then the onexception event is fired. Otherwise, either an oncomplete or an onerror is fired depending on the status of the XmlHttpRequest.
Since our test code does not possibly generate any exceptional cases, the only option left is that we are falling back to the onerror handler. Let’s check this by modifying the test case:
...
var request = o2.Ajax.get(url, params, {
oncomplete: function() {
isDone = true;
assert(me, true, 'oncomplete fired.');
me.terminate();
},
onerror: function() {
assert(me, false, 'onerror fired.');
me.terminate();
}
});
...
After running the test suite once more, we will see that, onerror event is being fired.
It’s not a Bug, It’s a “Behavior by Design“
But, wait. Maybe that’s what’s supposed to happen.
o2.Ajax.get reserves success responses to only HTTP SUCCESS (200) and HTTP CACHED (304). Normally, an abort request completes with a readyState COMPLETED (4), and an HTTP status of UNINITIALIZED (0).
If I am aborting a request, I had better see it as a transport-level error, rather than a success response. So the outcome is not a bug, and it is an expected consequence.
That’s the beauty of writing unit tests:
- We slow down to dwell upon the situation,
- We think in depth and breadth,
- And this enables us make educated decisions about the expected outcome.
If we haven’t constructed the unit test, we would have never thought about this case at all.
Let’s rewrite the unit test to reflect this change:
add('o2.Ajax SHOULD return to onerror handler, after aborting a request', {
count: 2,
test: function() {
var me = this;
var url = 'service/service.php';
var params = {wait : true};
var isDone = false;
var request = o2.Ajax.get(url, params, {
onerror: function() {
isDone = true;
assert(me, true, 'onerror fired.');
me.terminate();
}
});
setTimeout(function() {
request.abort();
}, 500);
setTimeout(function() {
assertStrictEqual(me, isDone, true, 'Request is timely processed.');
me.terminate();
}, 2000);
}
});
After re-running the test with these modifications, everything is back in order:
PASS: All unit tests have been completed: (total success: 52, total failure: 0, total # of test: 21)
Case Closed? Don’t be that Sure
Indeed, @tunix helped me recognize the following bug, while developing an auto completion module for grou.ps.
Kudos to him!
Now that all unit tests in the test suite pass, I could sleep peacefully… Or I thought I could, until I ran the unit test in IE 9. M$ IE again did its thing and puked out one of its cryptic error messages just after aborting the request:
Could not complete the operation due to error c00c023f?! — I beg your pardon?
Since the test passes in the most recent versions of Opera, Safari, Chrome, Firefox, but fails in the most recent “standards-compliant” rversion of IE (surprise!) we can deduce that this is an IE-specific bug.
Further tests on the issue showed that, after aborting a request in IE 9, trying to read any of the properties of the corresponding XMLHttpRequest object resulted in the above error to be thrown.
So let’s modify the processCallbacks function in o2.ajax.js module to take care of this case:
/*
* <p>Processes callbacks and finalizes the <code>Xhr</code>.</p>
*
* @param {XMLHttpRequest} xhr - the current <code>Xhr</code> instance.
* @param {Object} callbacks - oncomplete, onerror and onexception callbacks.
*/
function processCallbacks(xhr, callbacks) {
var nillCached = nill;
var oncomplete = callbacks.oncomplete || nillCached;
var onerror = callbacks.onerror || nillCached;
var onexception = callbacks.onexception || nillCached;
var isSuccess = false;
var status = 0;
var responseText = '';
var responseXml = null;
var statusText = '';
// IE9 throws error when accessing these properties
// when the request is aborted.
try {
status = xhr.status;
responseText = xhr.responseText;
responseXml = xhr.responseXML;
statusText = xhr.statusText;
} catch (ignore) {
}
isSuccess = status === kOk || status === kCached;
callbacks = callbacks || {};
// Since the response has come, mark the request as "completed".
xhr.isComplete = true;
try {
if (isSuccess) {
oncomplete(responseText, responseXml, xhr, status);
return;
}
onerror(status, statusText, xhr);
} catch(ex) {
onexception(ex, xhr);
} finally {
finalizeXhr(xhr);
}
}
We’ve enclosed the read operations of the XMLHttpRequest into a try catch block, so that if we fail to read any property, a set of default values will be retained instead of getting a peculiar error.
In case of an exception while reading the status of the XMLHttpRequest, the default status will be 0 and the onerror handler will execute.
A discussion on this Google group thread says that the bug only occurs if you have both standards mode and ie9 rendering mode set.
This led me think the error may be due to Microsoft’s brand new native XMLHttpRequest object. Then I found out that if we use an ActiveX fallback instead (e.g. “Msxml2.XMLHTTP“) the bug does not occur.
To support forward compatibility, I prefer the above try-catch solution instead of using an older version of the component; hoping the Microsoft will fix this bug in the upcoming releases of Internet Explorer (IE 17 maybe
).
Aborting Requests on Purpose
A request can be programmatically aborted by calling XMLHttpRequest.abort() method, or it can abruptly abort due to a network connectivity problem. For our current o2.Ajax object, both cases will be handled by the same onerror callback.
But what if we purposefully abort a request, and want to handle it in a separate callback of its own? This can be done by adding another method to o2.Ajax object.
abort : function(xhr) {
if(!xhr || xhr.isAborted) {
return;
}
try {
xhr.isAborted = true;
xhr.abort();
} catch (ignore) {
}
}
Then we will check for the isAborted flag and execute an onaborted callback, if we catch that we intentionally aborted. Here’s the final version of the processCallbacks function:
function processCallbacks(xhr, callbacks) {
var oncomplete = callbacks.oncomplete || nill;
var onerror = callbacks.onerror || nill;
var onexception = callbacks.onexception || nill;
var onaborted = callbacks.onaborted || nill;
var isSuccess = false;
var status = 0;
var responseText = kEmpty;
var responseXml = null;
var statusText = kEmpty;
if(xhr.isAborted) {
onaborted(xhr);
return;
}
// IE9 throws error when accessing these properties
// when the request is aborted.
try {
status = xhr.status;
responseText = xhr.responseText;
responseXml = xhr.responseXML;
statusText = xhr.statusText;
} catch (ignore) {
}
isSuccess = status === kOk || status === kCached;
callbacks = callbacks || {};
// Since the response has come, mark the request as "completed".
xhr.isComplete = true;
try {
if (isSuccess) {
oncomplete(responseText, responseXml, xhr, status);
return;
}
onerror(status, statusText, xhr);
} catch(ex) {
onexception(ex, xhr);
} finally {
finalizeXhr(xhr);
}
}
These seemingly minor changes made o2.Ajax more robust and flexible. Without the aid of unit tests, we wouldn’t have made this exhaustive analysis, and may never have seen the big picture at all.
Conclusion
In this article, we’ve seen an example of how unit testing can help us create and maintain a robust code, with predictable outcomes.
I restrain myself from saying “Unit testing helps you create bug-free code“, because, contrary to the belief of many, unit testing and test-driven development is not about finding bugs.
Never the less, it’s an excellent tool to keep your code stable, and find environment-related problems, and regression problems that you may easily overlook.
…
Satisfied with this outcome, I’m going to go back to my sublime text, and lay a siege to completing the test suites of o2.js
As always, I’d love to hear your comments and suggestions:
- Do you find unit testing useful?
- Have you found surprising things after running a test suite?
- Do you think that test-driven-development is a paradigm, when applied correctly, will reduce the maintenance costs of your software?
- Or do you think blindly and obsessively applying test-driven-development practices is an “overkill”?


