Unit Testing jQuery Plugins with Grunt and QUnit

Filed under: jQuery, web development

comments (6) Views: 14,910

For the last 4 or 5 years I've been hearing about this thing called unit testing. It's something that every developer says they want to do, but never have the time for. The relative importance of unit testing is proven when you see it fall beneath documentation on the priority scale. Regardless, as I continue to progress in my career there are certain things that simply must be vetted and conquered. Learning MVC (via Ember.js) is one of them and unit testing is another. This blog post will talk about getting started with unit testing using QUnit and Grunt, an excellent command line tool for Mac and Windows.

The first thing you'll need to do is to install Grunt, I mean install Node.js, I mean be comfortable with the command line. Wait wait wait, don't leave...I know the command line is scary...I used to be afraid of it just like you, but then I just decided one day to start using it. Now I usually have 3 or 4 terminal windows open at all times...tailing log files, running servers, even editing the occasional text file in VIM. Not bad for someone who used to complain about terminal windows right? Anyway, let's get back on track. This article won't cover installing Node.js. If you can install Node.js then you're in good shape to read the rest of the article.

Now, where were we? That's right...installing Grunt. Since you already have Node you'll just need to issue a single command to get the ball rolling.

npm install -g grunt

Npm stands for Node Package Manager, the primary way of obtaining libraries for use with Node. The -g tells Node to install grunt globally so that all users on your machine will have access to it. Once Grunt tells you that it's been installed successfully, it's time to put it to work. Grunt has the following pre-defined tasks available to it:

  • concat - concatenates files
  • init - Generate project scaffolding from a predefined template
    • gruntfile
    • commonjs
    • jquery
    • node
  • lint - validates files using JSHint
  • min - minifies files using UglifyJS
  • qunit - runs unit tests using QUnit
  • server - starts a static web server
  • test - Run unit tests with Nodeunit
  • watch - Run predefined tasks whenever watched files change

Running a task is as simple as grunt <task>, and in the case of creating a new template only a tiny bit more complex: grunt <task>:<template>. We'll be using Grunt to create a jQuery plugin template so input the following then hit enter: grunt init:jquery. Grunt takes the driver's seat and walks you through the process of naming your project, giving it a file name, a Git repository, a description and all sorts of goodies. Once you've answered all of Grunt's questions it will spit out a slew of files. Note that while Grunt does set up some basic Git stuff on your behalf you'll still need to create the Git repo manually.

Please answer the following:
[?] Project name (jquery.grunt-test) 
[?] Project title (jQuery Grunt Test) 
[?] Description (The best jQuery plugin ever.) 
[?] Version (0.1.0) 
[?] Project git repository (git://github.com/andymatthews/jquery.grunt-test.git) 
[?] Project homepage (https://github.com/andymatthews/jquery.grunt-test) 
[?] Project issues tracker (https://github.com/andymatthews/jquery.grunt-test/issues) 
[?] Licenses (MIT GPL) 
[?] Author name (andy matthews) 
[?] Author email (andy@commadelimited.com) 
[?] Author url (none) 
[?] Required jQuery version (~1.5) 
[?] Do you need to make any changes to the above before continuing? (y/N) 

Writing grunt.js...OK
Writing libs/jquery/jquery.js...OK
Writing libs/qunit/qunit.css...OK
Writing libs/qunit/qunit.js...OK
Writing README.md...OK
Writing src/jquery.grunt-test.js...OK
Writing test/jquery.grunt-test.html...OK
Writing test/jquery.grunt-test_test.js...OK
Writing LICENSE-MIT...OK
Writing LICENSE-GPL...OK

Initialized from template "jquery".

Done, without errors.

The three files we'll be focusing on are the plugin file in the src directory and the HTML and JS files within the test directory. Open those three files in the text editor of your choice...a good option is Sublime Text 2 for both Mac and Windows.

Let's take a look at the plugin file first. It should look something like this:

We're only going to work with the collection method section so delete the Static method and Custom selector sections. What you begin with should look like this (note we're keeping the default plugin name for simplicity's sake):

Before we start writing our tests I have to confess that I wasn't quite honest with you about Grunt and QUnit. Grunt runs with with Node.js but requires one additional piece of software to run unit tests. It's called PhantomJS and it's basically a headless WebKit rendering engine. That means that it can be run via the command line and used to test things without having an entire browser open. You only need to install PhantomJS if you want to be able to run your unit tests from Grunt. Otherwise you can simply drag the HTML file from your tests directory into a web browser and it'll run just fine. Install PhantomJS or don't, it's up to you.

Now, the premise of unit testing is that you write failing tests first, then write the code which makes your tests pass. This plugin will be very simple, in fact it's only going to do one thing, add a class to the specified text. This means that testing will be very simple. Open the JS file contained within the tests directory...you'll see a lot of code in there but we're going to delete everything contained within the outer function block and start with a single test, containing two assertions.

Now we come to the multiple choice part of the article. If you've installed PhantomJS, input the command grunt qunit. If you decided not to install PhantomJS then open the HTML file in the tests directory in your browser. Running Grunt will give you this:

Running "qunit:files" (qunit) task
Testing jquery.grunt-test.htmlF
>> chainable
>> Message: Died on test #1: 'undefined' is not an object

<WARN> 1/1 assertions failed (21ms) Use --force to continue. </WARN>

Aborted due to warnings.

Whereas running QUnit in your browser gives you this:

Both of them are telling you the same thing...our test failed...and that's a good thing because now we can fix it. But first let's examine exactly what this QUnit is doing. Keep in mind that QUnit is ALL JavaScript. That means that anything you can do in JavaScript can be done within this test file. When testing your code it's best to test the smallest pieces possible. Not only will your test be simpler, but there will be less chance of mistake. The first line creates a test suite, in our case called "chainable", then executes an anonymous function which contains two assertions.

The "ok" assertion tests for a true false value. The first part of the test executes a statement while the second half contains what we expect is happening. This code tests if our plugin is chainable but attaching itself to a jQuery selector, then attempting to run a chained jQuery method. If all was well our plugin would perform its business then allow additional code to execute. But that can only happen if the plugin returns the objects passed into it. Looking back at our plugin the function block is empty. We'll fix that by adding the following code into the function. Rerun your tests after adding this block and you should see one passing test and one failing.

// Collection method.
$.fn.awesome = function() {
	return this.each(function() {

	});
};

The "equal" assertion compares two values using JavaScript's == operator (QUnit also has a "strictEqual" test which uses the stricter === operator). The first part executes a statement, the second part specifies the result we expect to find, while the third part describes what the test is doing. We already know that our plugin can be chained, but the second test is failing because it's actually looking for the "testing" class on text which doesn't exist. Remember the HTML file within the tests directory? That file is called a "test harness". It allows developers to include a chunk of HTML which specifically relates to their test. In our case we need a paragraph of text containing one or more bold tags. Open the HTML file and look for a div tag with an ID of qunit-fixture. Make sure your HTML file contains the required text then rerun your tests. They should both be passing now. Congratulations!

We'll add one more test to make sure our plugin is doing it's job correctly. After the chainable test add the following three lines of code. Confirm the test fails by rerunning your tests.

test('functionality', function(){
	equal($("p b").awesome().hasClass("awesomesauce"), true, "plugin adds the correct class");
});

This test makes sure that a specific class is being added to the text we're passing in. Getting the test to pass is simplicity itself. The plugin simply takes the selector passed to it and adds the awesomesauce class to all bold tags within paragraph tags.

return this.each(function() {

	$(this).addClass('awesomesauce');

});

Now that your masterpiece of a jQuery plugin is complete, Grunt has even more love for you. Simply type grunt at the command line and Grunt will run all your unit tests, run JSLint against your code, and finally minify your plugin file. Assuming all goes well after you type the command you'll end up with a nicely compact, perfectly valid, functional plugin file in a newly created dist directory.

Unit testing might seem like more trouble than it's worth now, but being able to come back to code you wrote 6 months ago and see your tests still passing must be hugely rewarding. Not to mention the comfort in knowing that any bugs you introduce in old code by writing new code will get caught by your existing tests. Finally, unit testing is the first step towards continuous integration. Consider how valuable it could be to write code, or fix a bug, run your entire suite of tests against that code and then roll it out to production the SAME DAY because you're supremely confident your code is solid. I hope this gives you hope that unit testing can be within your grasp. Now go forth and test!

Amazon logo

If this article was interesting, or helpful, or even wrong, please consider leaving a comment, or buying something from my wishlist. It's appreciated!

comments powered by Disqus