Unit testing a jQuery plugin with QUnit
QUnit is a powerful, yet easy-to-use JavaScript unit testing framework. It is used by the jQuery, jQuery UI and jQuery Mobile projects and it is well suited for testing a typical jQuery plugin. This article builts upon Getting Started with QUnit, How to Test Asynchronous Code with QUnit and of course the official Introduction to Unit Testing articles so feel free to skim through those first and return to see how QUnit can be used to test an already built jQuery plugin, and get you started with unit testing.
This article should be considered slightly outdated. The QUnit is actually a relatively healthy project at GitHub, as is jQuery, but I think that jQuery itself is not really relevant these days. Other than that, the general idea of the article remains.
Installation
You should start with a mostly empty HTML file, where the qunit-2.0.0.js and qunit-2.0.0.css files will be included, along with the plugin to be tested. I have a simple (and incomplete) Accordion plugin at GitHub for the purpose, built on a solid pattern with just enough functionality (attaches click events, manipulates the DOM, executes callbacks, provides a public method) to give me something to test. Since the plugin was not built with test driven techniques I will have to unit test it as it is, and see if it works as planned. You are free to improve the plugin, add functionality if you like, but this is beyond the scope of this article. On the other hand, there are already established, well-built plugins for the purpose, like jQuery Accessible Accordion System, using ARIA or Frend Accordion (vanilla JavaScript actually) so it may not be worth it. But let’s test it first!
Testing
The HTML file (let’s call it test.html
) needs a couple of empty div
s: a <div id="qunit"></div>
and a <div id="qunit-fixture"></div>
. Inside #qunit-fixture
you should insert the HTML that the plugin will act upon, and it will be reset to the this state before each test. Those tests could be written in a tests.js
file (like the one I have on GitHub), or included inline in the same HTML file. The actual tests are included in QUnit.test()
methods.
Plugin registration
QUnit.test( 'Registration', function( assert ) {
assert.ok( $.fn.accordion, 'registered as a jQuery plugin' );
var defaults = {
headerElement: 'dt',
contentElement: 'dd',
preselected: 'expanded',
onCollapse: $.noop,
onExpand: $.noop,
onInit: $.noop,
onDestroy: $.noop
};
assert.equal( $.fn.accordion.defaults, defaults.toString(), 'default settings are available' );
} );
The first test (“Registration”) is checking whether the plugin was successfully initialized and that default settings are globally available. I used the ok
assertion to test for a “truthy” condition, like the existence of $.fn.accordion
that registers when the plugin is loaded. Default settings are then tested using the equal
assertion, on a serialized defaults
object, to test that plugin defaults are globally available to the developer.
Setting global defaults
QUnit.test( 'Default settings', function( assert ) {
var $faq = $( '.faq' ),
defaults = $.fn.accordion.defaults,
newDefaults = $.extend( {}, $.fn.accordion.defaults,
{
headerElement: '.accordion-header',
contentElement: '.accordion-panel'
}
);
$.fn.accordion.defaults = newDefaults;
$faq.accordion();
assert.propEqual( $faq.data( 'plugin_accordion' ).settings, newDefaults, 'set new default settings' );
$.fn.accordion.defaults = defaults;
} );
During the initialization of the plugin, the default settings are globally available at $.fn.accordion.defaults
and can be changed to whatever is needed, for every instance of the plugin in the page. The “Default settings” test sets a couple of new settings, overwriting the default elements that the accordion plugin uses. The plugin should see these new settings and adopt them. The propEqual
assertion compares the properties of new settings object to the settings
object that the plugin was initialized with (available as $( ACCORDION_ELEMENT ).data( 'plugin_accordion' ).settings
), and those objects should have the same properties. The default defaults are restored afterwards, ready for the next tests.
Chainability
QUnit.test( 'Chainability', function( assert ) {
var $faq = $( '.faq' );
assert.ok( $faq.accordion().addClass( 'testing' ), 'can be chained' );
assert.ok( $faq.hasClass( 'testing' ), 'successfully chained' );
} );
jQuery plugins should be chainable, meaning that when initialized on a DOM element, the same element should be returned, available for the next method / plugin to act upon. I use the ok
assertion again and see if my plugin can be successfully chained. This test might not be needed if your plugin is based on a solid boilerplate, but I have it here for completness’ sake.
Adding HTML attributes
QUnit.test( 'ARIA attributes', function( assert ) {
var $faq = $( '.faq' );
$faq.accordion();
assert.ok( $faq.is( '[role="tablist"]' ), 'added role' );
assert.ok( $faq.is( '[aria-multiselectable="false"]' ), 'is not aria-multiselectable' );
} );
ARIA attributes are added by the plugin to the elements, to make the whole thing accessible to assistive technologies. A bunch of ok
assertions can be used to check for the correct initialization of various attributes on the DOM elements. I have not included all the possible tests above for brevity. I run through all header and content elements, and check with ok
assertions for the addition of the correct ARIA attributes on each and every DOM element manipulated by the plugin.
Testing user interactions
QUnit.test( 'Callbacks', function( assert ) {
var $faq = $( '.faq' );
$faq.accordion( {
onCollapse: function( $header, $content ) {
assert.ok( $header.is( '[aria-selected="false"][aria-expanded="false"]' ), 'collapsed header:' + $header[0].id );
assert.ok( $content.is( '[aria-hidden="true"]' ), 'collapsed content:' + $content[0].id );
},
onExpand: function( $header, $content ) {
assert.ok( $header.is( '[aria-selected="true"][aria-expanded="true"]' ), 'clicked header:' + $header[0].id );
assert.ok( $content.is( '[aria-hidden="false"]' ), 'expanded content:' + $content[0].id );
}
} );
$( $faq.data( 'accordion_headers' ) ).first().trigger( 'click' );
$( $faq.data( 'accordion_headers' ) ).first().trigger( 'click' );
} );
User interactions with the DOM must be triggered and/or faked. We can easily trigger click events, and see whether a callback is indeed called, getting back the expected parameters, and that the correct attributes are indeed applied. The ok
assertion is again used, to check if clicking on a header is working as expected. I test onCollapse
and onExpand
callbacks by triggering a click event on the first header element twice.
Clean-up
QUnit.test( 'Cleanup', function( assert ) {
var $faq = $( '.faq' );
$faq.accordion();
$faq.accordion( 'destroy' );
assert.notOk( $faq.data( 'plugin_accordion' ), 'destroyed successfully' );
} );
The destroy
method is called on the element that the accordion was previously initialized upon, and the notOk
test is checking for the existence of the data that the plugin saves on the element, after being “destroyed”
This is how the test.html
looks like, if all the tests are successful. Well done!
Conclusion
Hopefully you can see that QUnit is an easy framework to get you started with unit testing, and how it can be used to test your already written jQuery plugins. Once you get the hang of it, you can move to test-driven development and maybe explore other, more advanced frameworks.