Unit testing with Javascript
I’m a big fan of unit testing. Everybody who has been working with me knows it. Unit testing makes you sleep well at night. If you have not understood the advantage of unit testing your code you should really try to do it!
Lately I’ve been working a lot with Javascript for obvious reasons. Ajax is very hype and if more and more websites require a deep knowledge of the Javascript secrets.
Javascript is not hard but some common mistakes can make your development a pain. Projects get larger and larger and you really need to have good tools to help you not having to pray every time you make a small change.
I will not try to convince you to test your code (maybe I will try in an other article) but I will show you a couple of tricks I use when testing my code.
To run the unit tests I use jsUnit which is a port of the famous jUnit. It is pretty simple to install and if you have used jUnit you basically know how to use it.
Javascript is a dynamic language. It means that you can change the prototype of an object after the object has been defined. That means that if you have an object for which a given method is defined, you can add methods on the fly. And you can do for only one instance or for all instances! Let’s give an example:
[source:js]
// define an object called Hello
function Hello() { }
// define a property of the object of class Hello of type function
Hello.prototype.scream = function() { alert(“Hello!” );}
// Hello!
var s = new Hello();
s.scream(); //shows the message “Hello!”
// Let’s add a new method to the instance s (and only to that)
s.bye = function() { alert(“Good bye!”);}
s.bye(); //shows the message “Good bye!”
[/source]
That was pretty easy and straightforward if you know the basic of object oriented Javascript.
The cool stuff is that you can use the dynamicity of Javascript to make your testing very simple. Mocking has never been easier!
Let’s make and example and test the following object:
[source:js]
// Constructor
function DiscountCalculator(item) {
this.discount = discount;
this.item = item;
}
// Calculates the discount price for the given item
DiscountCalculator.prototype.discountPrice= new function() {
this.getPrice()*(100-this.getDiscount())/100;
}
DiscountCalculator.prototype.getPrice= new function() {
// “complex” ajax call to retrieve the price of the current item
//implementation omitted
}
DiscountCalculator.prototype.getDiscount= new function() {
// “complex” ajax call to retrieve the discount of the current customer
//implementation omitted
}
[/source]
The DiscountCalculator object has basically one method which is useful: discountPrice.
“getPrice” and “getDiscount” are two ajax calls that could be implemented with DWR or any other remoting technique. Since we want to run the test without a remote server providing some real values, we have to mock those calls.
Now, lets write our test. This is the specifiation of the test:
“If an item costs 100 and I get 10% discount I will pay 90”
[source:js]
function testDiscountPrice() {
var discountCalculator = new DiscountCalculator(“some item”);
discountCalculator.getPrice = function() { return “100” };
discountCalculator.getDiscount = function() { return “10”};
var discountPrice = discountCalculator.discountPrice();
assertEquals(”discountPrice is 90”, 90, discountPrice);
}
[/source]
Can you believe it? What I did is to simulate the two ajax calls by changing the prototype of the “discountCalculator” instance. I replaced the “complex” ajax calls with a simpler implementation that returns values which I can control. Do you remember “If an item costs 100 and I get 10% discount I will pay 90”? I just simulate the “if” part mocking my object.
That is pretty cool! To do that in java or in an other non dynamic language it would have needed a couple of other objects around…
Lately I’ve been working a lot with Javascript for obvious reasons. Ajax is very hype and if more and more websites require a deep knowledge of the Javascript secrets.
Javascript is not hard but some common mistakes can make your development a pain. Projects get larger and larger and you really need to have good tools to help you not having to pray every time you make a small change.
I will not try to convince you to test your code (maybe I will try in an other article) but I will show you a couple of tricks I use when testing my code.
To run the unit tests I use jsUnit which is a port of the famous jUnit. It is pretty simple to install and if you have used jUnit you basically know how to use it.
Javascript is a dynamic language. It means that you can change the prototype of an object after the object has been defined. That means that if you have an object for which a given method is defined, you can add methods on the fly. And you can do for only one instance or for all instances! Let’s give an example:
[source:js]
// define an object called Hello
function Hello() { }
// define a property of the object of class Hello of type function
Hello.prototype.scream = function() { alert(“Hello!” );}
// Hello!
var s = new Hello();
s.scream(); //shows the message “Hello!”
// Let’s add a new method to the instance s (and only to that)
s.bye = function() { alert(“Good bye!”);}
s.bye(); //shows the message “Good bye!”
[/source]
That was pretty easy and straightforward if you know the basic of object oriented Javascript.
The cool stuff is that you can use the dynamicity of Javascript to make your testing very simple. Mocking has never been easier!
Let’s make and example and test the following object:
[source:js]
// Constructor
function DiscountCalculator(item) {
this.discount = discount;
this.item = item;
}
// Calculates the discount price for the given item
DiscountCalculator.prototype.discountPrice= new function() {
this.getPrice()*(100-this.getDiscount())/100;
}
DiscountCalculator.prototype.getPrice= new function() {
// “complex” ajax call to retrieve the price of the current item
//implementation omitted
}
DiscountCalculator.prototype.getDiscount= new function() {
// “complex” ajax call to retrieve the discount of the current customer
//implementation omitted
}
[/source]
The DiscountCalculator object has basically one method which is useful: discountPrice.
“getPrice” and “getDiscount” are two ajax calls that could be implemented with DWR or any other remoting technique. Since we want to run the test without a remote server providing some real values, we have to mock those calls.
Now, lets write our test. This is the specifiation of the test:
“If an item costs 100 and I get 10% discount I will pay 90”
[source:js]
function testDiscountPrice() {
var discountCalculator = new DiscountCalculator(“some item”);
discountCalculator.getPrice = function() { return “100” };
discountCalculator.getDiscount = function() { return “10”};
var discountPrice = discountCalculator.discountPrice();
assertEquals(”discountPrice is 90”, 90, discountPrice);
}
[/source]
Can you believe it? What I did is to simulate the two ajax calls by changing the prototype of the “discountCalculator” instance. I replaced the “complex” ajax calls with a simpler implementation that returns values which I can control. Do you remember “If an item costs 100 and I get 10% discount I will pay 90”? I just simulate the “if” part mocking my object.
That is pretty cool! To do that in java or in an other non dynamic language it would have needed a couple of other objects around…
I have being doing unit tests on JavaScript using the SimpleTest library, as I am enjoying coding JavaScript using MochiKit.
ReplyDeleteIf you want, you can take a look at the javascript crypto library we have released as part of our main project on Google Code:
http://code.google.com/p/clipperz/
Most of the code is "covered" by tests; lately I have included some "performance" tests (bound to the execution time), and so they may fail depending on the computer/browser where you run them.
This is far from ideal, but I had no better idea on how to test while working on improving the performance of the code.
If only I was able to "cover" with test also the application code. :-(
I will try to think better at your suggested "mock" style, to see if it is suitable for my code base too.
Thanks for sharing your insights.