Matthew Setter is a professional technical writer and passionate web application developer. He’s also the founder of Malt Blue, the community for PHP web application development professionals and PHP Cloud Development Casts – learn Cloud Development through the lens of PHP. You can connect with him on Twitter, Facebook, LinkedIn or Google+ anytime.
Summary
As developers, we’re taught the importance of testing right from the beginning. Testing helps us write better code in less time and makes us feel more comfortable with the eventual outcome. I agree with all of these benefits. But sadly, I don’t always do it. And my guess is that you don’t always test either.
What’s more, I haven’t done a lot of JavaScript testing. Like a number of developers, I’m guilty of leaving JavaScript as an afterthought, whereas my PHP, Bash and Python code are first-class citizens.
However, I’ve started building more complex, object-driven code. As the complexity grows, it’s become rather obvious that it needs to be covered by proper unit tests. The question was how. So I started looking around for the best available JavaScript unit testing libraries and came across the following:
* QUnit
* Mocha
* Jasmine
* Sinon.JS
They all seem to offer solid feature sets for testing JavaScript. So to improve my JavaScript code quality and help you with yours, I’ll walk through each of them in a series of articles on the New Relic blog.
We’ll cover how the testing units work and what they can do, along with what they’re like to use. You’ll get an informed and honest opinion before you try them out with your own code. Sound good? Let’s start with QUnit.
Installing QUnit
QUnit is a self-contained library, requiring only one JavaScript file (qunit.js) and one CSS file (qunit.css). You can either download them from the QUnit site or clone them from the QUnit GitHub repository, as follows:
[code language="text"]
git clone git://github.com/jquery/qunit.git
[/code]
After you get a copy of the files, add them into your project web root and include them directly or with the framework or application that you’re already using (such as Zend Framework or Oxid eSales). Then you're ready to start testing.
The JavaScript Code
This isn't a complex example, but I believe it's enough to write some meaningful tests. The following code example creates a jQuery object that contains the properties and functions required to perform a basic mortgage calculation. Let's walk through it slowly to give you an overview of how it works.
[code language="javascript"]
$(document).ready(function() {
var rateCalculator = {
[/code]
The properties of the object store the following information (pre-initialized with some meaningful defaults):
* The amount of the loan
* The regular repayment amount
* The repayment frequency (monthly, fortnightly, weekly)
* The term of the loan
* The current interest rate of the loan
The defaults:
[code language="javascript"]
loanAmount: 150000,
repaymentAmount: 700,
repaymentFrequency: "monthly",
loanTerm: 35,
interestRate: 9.15,
[/code]
We then have a simple function that allows us to override the existing defaults with the information specified in the form, which we'll see later.
[code language="javascript"]
setLoanProperties: function(loanAmount, repaymentFrequency, loanTerm, interestRate) {
this.loanAmount = new Number(loanAmount);
this.repaymentFrequency = repaymentFrequency;
this.loanTerm = new Number(loanTerm);
this.interestRate = interestRate / 100;
console.log(this);
},
[/code]
The calculateDifference
method uses a mortgage calculation formula that I found on Wolfram. It calculates the recurring payment amount and the total repayable amount.
It initializes a series of local variables as Number
objects and uses the pow
function from the Math
library to complete the calculation. I've split the calculation across a number of variables, more for readability and maintainability than anything else.
[code language="javascript"]
calculateDifference: function() {
switch (this.repaymentFrequency) {
case ("weekly"):
var period = 52;
break;
case ("fortnightly"):
var period = 26;
break;
case ("monthly"):
default:
var period = 12;
break;
}
var rateByPeriod = new Number(this.interestRate / period);
var numerator = new Number(Math.pow((1 + rateByPeriod), (this.loanTerm * period)));
var denominator = new Number(numerator.toFixed(2) - 1);
var regularRepayment = new Number(this.loanAmount * rateByPeriod * (numerator / denominator));
var totalAmount = new Number(regularRepayment * (period * this.loanTerm));
$("#repaymentAmount").html(regularRepayment.toFixed(2));
$("#totalToRepay").html(totalAmount.toFixed(2));
},
};
[/code]
When the calculation is complete, it displays the two calculated values in the form so the user can see them. The values the user enters in the form are used to initialize the object and then to calculate the amounts. There's no validation in the form, just the key functionality required.
[code language="javascript"]
$("#submit").click(function() {
rateCalculator.setLoanProperties($("#originalAmount").val(), $("#repaymentFrequency").val(), $("#loanTerm").val(), $("#interestRate").val());
rateCalculator.calculateDifference();
});
});
[/code]
The HTML itself is rather trivial, so I won't spend time here going through it. It's available with the rest of the code.
QUnit Functions
Although simple in scope, the QUnit Framework is rather effective. Unlike other unit testing frameworks such as PHPUnit or SimpleTest, it doesn't have a lot of methods and assertions.
That's not to say that isn't fully capable. This is a list of the key tests available:
FUNCTION | DESCRIPTION |
ok | Assert that a statement is "truthy" |
equal | Assert that two values are equivalent to each other using non-strict comparison. i.e., == |
strictEqual | Make a strict assertion that two values are equivalent to each other. i.e, === |
test | Add a test to the list of tests to run |
asyncTest | Add a test for an asynchronous section of code |
throws | A test to run in the event of an exception being thrown |
While I haven’t used all the tests above, they show that QUnit is a solid package for testing. What's more, we can use them in combination with the power of jQuery and JavaScript to do what we need to do. Now let's walk through the unit tests to see how they work.
The Basic Unit Test
Tests can be grouped together into modules. Any call to test
– which is preceded by a call to module
– is considered part of a group until the next call to module or the end of the test suite. I've done that below and included the classic setup and teardown functions. Actually, I've done nothing in either of them, but you can use these functions as needed to set up your DOM or do any other work.
[code language="javascript"]
module( "module", {
setup: function() {
//ok( true, "one extra assert per test" );
}, teardown: function() {
//ok( true, "and one extra assert after each test" );
}
});
[/code]
Another good thing about using QUnit is that you can set up tests to run as an atomic unit. I've broken the testing for my code into two parts:
1. Check the availability of the required form elements.
2. Ensure that the calculation operates as expected with a correct set of inputs.
The first test checks to make sure that all the required form elements are in place. I used simple CSS Selectors to access the elements in question, then checked that the length property is not equal to zero – which is a simple way to ensure it’s present.
The second element is the message for when the test passes or fails, so you know which one worked or didn't. In the call to test
, I provided a meaningful description for it (which is output when the test runs), the number of tests to expect and an anonymous function containing the tests.
[code language="javascript"]
// ensure all the required form elements are in place
test( "All required form elements exist", 7, function() {
ok($("#originalAmount").length != 0, "original amount element exists");
ok($("#repaymentFrequency").length != 0, "repayment frequency element exists");
ok($("#loanTerm").length != 0, "loan term element exists");
ok($("#interestRate").length != 0, "interest rate element exists");
ok($("#submit").length != 0, "submit button element exists");
ok($("#repaymentAmount").length != 0, "total repayment amount output element exists");
ok($("#totalToRepay").length != 0, "total amount to repay output button element exists");
});
[/code]
As I did in the first test, I've provided a description for the second one: the number of tests that will run and an anonymous function containing the tests. In this one, I've provided a bit of setup, which I could have provided in the setup
method. I initialized variables to predefined values and then set the various form elements accordingly.
After that, I leveraged QUnit’s ability to manually build an event object that can trigger the click event on the Submit button as if the user had done it. Assuming the events fire and the values are generated correctly, I've run two tests with the QUnit equal
method to check that the calculated values are what I expected.
[code language="javascript"]
test( "test calculation output", 2, function() {
var dummyLoanAmount = 270000;
var dummyYearsRemaining = 30;
var dummyInterestRate = 8.00;
$( "#originalAmount" ).val(dummyLoanAmount);
$( "#loanTerm" ).val(dummyYearsRemaining);
$( "#interestRate" ).val(dummyInterestRate);
var event,
$submitButton = $( "#submit" );
// trigger event
event = $.Event( "click" );
event.keyCode = 9;
$submitButton.trigger( event );
var monthlyRep = $( "#repaymentAmount" ).val();
var totalRep = $( "#totalToRepay" ).val();
equal(monthlyRep, 457.04, "monthly repayment amount correctly totals" );
equal(totalRep, 712985.68, "total repayment amount correctly totals" );
});
[/code]
The Test Output
Now that we’ve set up the tests and run them, how do they look? In the screenshot below you can see that the tests ran and the first test suite passed, but there's a problem in the second one.
I expected the DOM to get updated after the click event was triggered, just like when a user clicks the Submit button. Then I could compare the calculated values against the expected amounts.
But, according to QUnit, the value of the two div
elements is empty. So I'd like to throw this one out to you. Have you had a similar experience? If so, what solution did you come up with? Is it something small that I've missed? Let me know in the comments.
Enhancing the Tests
We’ve covered the core of the library. But let's take it further. QUnit has a number of functions that allow us to register callbacks around various events in the unit testing lifecycle. They are:
EVENT | DESCRIPTION |
begin | Fire when the test suite begins |
done | Fire when the test suite ends |
log | Fire when an assertion completes |
moduleDone | Fire when a module completes |
moduleStart | Fire when a module starts |
testDone | Fire when a test completes |
testStart | Fire when a test starts |
To create a simple example, I’ve implemented all of them in the code below. I’ve rerun the code with them in place so you can see the output.
[code language="javascript"]
QUnit.begin(function( details ) {
console.log( "Test Suit Starting." );
});
QUnit.done(function( details ) {
console.log( "Test Suit Ending. Results: Total: ", details.total, " Failed: ", details.failed, " Passed: ", details.passed, " Runtime: ", details.runtime );
});
QUnit.log(function( details ) {
console.log( "Assertion complete. Details: ", details.result, details.message );
});
QUnit.moduleStart(function( details ) {
console.log( "Starting module: ", details.module );
});
QUnit.moduleDone(function( details ) {
console.log( "Finished Running Module: ", details.name, "Failed/total: ", details.failed, details.total );
});
QUnit.testStart(function( details ) {
console.log( "Now Running Test: ", details.module, details.name );
});
QUnit.testDone(function( details ) {
console.log( "Finished Running Test: ", details.module, details.name, "Failed/total: ", details.failed, details.total, details.duration );
});
[/code]
By displaying the console window, you can see all the events were intercepted and view their respective details. Now granted, this isn't always necessary. But it can be handy to have when debugging the tests -- especially if you want to know as much as possible about what's going on in your test suite. Full credit to the QUnit manual for a number of the test examples above.
Conclusion
Whew! This was a rapid introduction to QUnit, one of the simplest and most effective unit testing libraries for JavaScript. What did you think? I realize that I haven’t covered everything available, but it's more than enough to get started.
Personally, I was surprised by its functionality. I had expected it to be less capable that what I found it to be. One thing to note, I'm not sure how QUnit will fare if you run it from the command line or include it in an automated testing workflow.
There's a post from 2010 about QUnit and the command line, which indicates that it's rather difficult to do. The post mentions that the library contains a lot of browser-related assumptions. I've wondered whether using PhantomJS could get around any of these aforementioned issues, but I haven't had time to test it.
A few more questions before I wrap up: Have you used QUnit in an automated context? Do you use QUnit as a normal part of your development workflow? We'd like to hear about your experience with QUnit in the comments below.
Next Time
I hope this article gave you a good idea of what's possible with unit testing in JavaScript. In my next article, I'll show you another unit testing library. See you there.
Further Reading
* Mortgage Calculation from Wolfram
* JQuery Unit Testing on Github
The views expressed on this blog are those of the author and do not necessarily reflect the views of New Relic. Any solutions offered by the author are environment-specific and not part of the commercial solutions or support offered by New Relic. Please join us exclusively at the Explorers Hub (discuss.newrelic.com) for questions and support related to this blog post. This blog may contain links to content on third-party sites. By providing such links, New Relic does not adopt, guarantee, approve or endorse the information, views or products available on such sites.