Last time we talked about JavaScript test frameworks. But a test framework is no good if you don’t write testable code.
Writing testable JavaScript
As is the case with .NET code, one of the most important parts of testing is writing code that you can actually test. Let’s look at some untestable JavaScript code and what we can do to make it testable.
// Untestable JavaScript :(
function MyClass
{
this.showMessage = function(message)
{
$("#messageTextBox").text(message);
}
this.anotherShowMessage = function(message)
{
document.getElementById("anotherMessageTextBox").innerText = message;
}
}
This code is untestable because it directly references UI elements in a page by name. When you run unit tests using pretty much any JavaScript testing framework, you create a separate page to run your tests from. That means that your UI elements will not be on that page, so any code that references these elements by name will not be testable.
Here’s how you would make this testable:
// Testable JavaScript!
function MyClass()
{
var element;
this.getElement = function()
{
return element;
}
this.setElement = function(newElement)
{
element = newElement;
}
this.showMessage = function(message)
{
element.innerText = message;
}
}
// unit test
describe('When showing a message',
{
'The message should be displayed in the element': function()
{
var myClass = new MyClass();
myClass.setElement(new function() { this.innerText = null; });
myClass.showMessage('I love testing!');
value_of(myClass.getElement().innerText).should_be('I love testing!');
}
});
Notice how MyClass still has the notion of a UI element, but now the UI element is injected by whoever is using the class. And since JavaScript is a dynamic language, this is easy to test because we don’t need an actual UI element in our unit test, we just need an object that has an “innerText” variable on it. Creating said object is as easy as doing this:
var myFakeUIElement = new function() { this.innerText = null; }
This is what I can do in my test. I tell MyClass that its element is my fake UI element, I call showMessage(), and then I verify that the innerText variable of my fake UI element got set.
Another example:
// Untestable JavaScript :(
function MyClass()
{
this.showMessage = function(message)
{
alert(message);
}
}
I think this one is pretty obvious. If you try and test this method, you’re going to have an alert box popping up on the screen in the middle of your tests. Not going to work. Try this instead:
// Testable JavaScript
function MyClass()
{
this.showMessage = function(message)
{
this.showAlertBox(message);
}
this.showAlertBox = function(message)
{
alert(message);
}
}
describe('When showing a message',
{
'The message should be displayed in an alert box': function()
{
var alertMessage;
var myClass = new MyClass();
myClass.showAlertBox = function(message) { alertMessage = message; }
myClass.showMessage('I love testing!');
value_of(alertMessage).should_be('I love testing!');
}
});
Here I separated my concerns again — I created a separate method that only shows the alert box. I can “mock” this method by simply redefining it to whatever I want (in this case I’m just storing what was passed into it). Now I can test that when I call MyClass.showMessage(), I can verify that it called the method that would show an alert box.
Time for more sadness:
// Untestable JavaScript :(
function MyClass()
{
this.result = null;
this.loadCustomerData = function(customerId)
{
$.ajax({
type: "POST",
url: "CustomerService.asmx/LoadCustomer",
data: "customerId=" + customerId,
success: function(msg)
{
this.result = msg.d; // return value from web service
}
});
}
}
This is an asynchronous Ajax call using jQuery. The problem here is that because this is actually making an asychronous Ajax call, the method will return and our test code will run before the callback executes.
There are two ways to solve this:
Solution 1:
// Testable JavaScript
function MyClass()
{
this.result = null;
this.loadCustomerData = function(customerId)
{
$.ajax({
async: false,
type: "POST",
url: "CustomerService.asmx/LoadCustomer",
data: "customerId=" + customerId,
success: function(msg)
{
this.result = msg.d; // return value from web service
}
});
}
}
describe('When loading a Customer',
{
'The customer name should be returned': function()
{
var alertMessage;
var myClass = new MyClass();
myClass.loadCustomerData(1);
value_of(myClass.result).should_be('Jon');
}
});
What changed here? I added “async: false” to the $.ajax() call. Now jQuery will run this method synchronously and I will be able to test it.
Caveat: This violates the basic rules of “what is a unit test” because an external dependency is involved (in this case, my .asmx web service). You can decide for yourself whether this is bad or not. In my case, the test still runs lightning fast because all it does it return some random numbers and it never hits a database, so it doesn’t bother me too much. But if you have a more complicated web service that you’re running and it takes a long time to return data, you may not want to do it this way. Here’s another way to fix this:
Solution 2:
function MyClass()
{
this.result = null;
this.loadCustomerData = function(customerId)
{
var myClass = this;
this.makeAjaxCall(
"POST",
"CustomerService.asmx/LoadCustomer",
"customerId=" + customerId,
function(ajaxResult) { myClass.result = ajaxResult; });
}
this.makeAjaxCall = function(type, url, data, successCallback)
{
var result;
$.ajax({
type: type,
url: url,
data: data,
success: function(msg)
{
successCallback(msg.d); // return value from web service
}
});
return result;
}
}
describe('When loading a Customer',
{
'The customer name should be returned': function()
{
var ajaxType;
var ajaxUrl;
var ajaxData;
var ajaxSuccessCallback;
var alertMessage;
var myClass = new MyClass();
myClass.makeAjaxCall = function(type, url, data, successCallback)
{
ajaxType = type;
ajaxUrl = url;
ajaxData = data;
ajaxSuccessCallback = successCallback;
};
myClass.loadCustomerData(1);
value_of(ajaxType).should_be('POST');
value_of(ajaxUrl).should_be('CustomerService.asmx/LoadCustomer');
value_of(ajaxData).should_be('customerId=1');
ajaxSuccessCallback('somevalue');
value_of(myClass.result).should_be('somevalue');
}
});
Slightly more complicated doing it this way, but if the situation warrants it, it’s worth it.
Next stop: we’ll do some test driven development and see this stuff in action!