Unit testing jQuery w/ QUnit

So, like most developers I love having unit test, but I hate writing them. Really figuring out the best way to write them and have them work well, is a pain in the ass, and that is what I hate. I have spent some time recently playing with QUnit and despite the number fact that I have found a number of posts online about it, none of them have provided (in my mind) a good way to test a site. The ideas used work well for testing a library, or specific functions, but I don’t write libraries or one off functions, I develop websites. So, I have come up with a method that I like that allows me to test my sites in a way that I like.

The Problems:

The unit testing information I have found mainly deals with testing libraries – this is no good for me.
Testing involves putting QUnit on your page, along with its specific markup – although you can have a “debug” mode for this, it just seems like a bad idea, for a number of reasons, including it not really being an accurate representation of your page, and it does not work across page requests.
Using headless testing – it is cool, but not cross browser or even really O/S ready.
Using something like Selenium – also cool, but involves building tests using a new tool that uses java.

My solution:

I have come up with my own solution, which, I admit is not perfect, but it does a darn good job of testing what I need tested. Basically it involves having a very simply html page next to your site, that loads your site in an iframe and runs the tests on it. It is cross-browser and cross-O/S safe, it works for pretty much any site, and uses the same javascript/jQuery I already write every day.

There are a couple of problems still with my solution, but they are pretty easy to overcome, and writing tests becomes very simple. The basic page we need to get started with the testing is this:

<html>
<head>
<title>QUnit tester</title>
<link type="text/css" href="/js/qUnit/qunit.css" rel="stylesheet" />
<script src="/js/qUnit/qunit.js" type="text/javascript"></script>
<script src="/js/jquery-1.4.4.min.js" type="text/javascript"></script>
<script type="text/javascript">
//set jquery to no conflict, so we do not have a problem with the version from in the page
var $$ = jQuery.noConflict(true);
var $ = jQuery = null; //we will be using the normal jquery vars soon enough
</script>

</head>
<body>

<div>
<h1>QUnit Tests</h1>

<div style="float: left">
<iframe height="700" width="1000" id="myFrame"></iframe>
</div>

<div style="float: right">
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<div id="qunit-fixture">test markup, will be hidden</div>

<input type="submit" value="Start Tests" id="startTests" />
</div>
</div>
</body>

<script type="text/javascript">
var pageNumber = 0;
$$('#myFrame').load(function () {
	//grab jQuery from inside the document
	$ = jQuery = window.frames[0].jQuery;

	//turn off async so tests will wait for ajax results
	$.ajaxSetup({ async: false });

	//turn off animations so they do no beark tests
	$.fx.off = true;

	//increase our page counter
	pageNumber++;

	//if we are not on the first page, find what tests to run
	if(pageNumber > 1) {

	switch(pageNumber) {
		case 2:
		page2tests();
		break;
		case 3:
		page3tests();
		break;
	}

	} else {
		//else, start the tests when they hit the run button
		$$('#startTests').click(function (e) {
			page1tests();
			return false;
		});
	}
});

function page1tests() {
module("Page 1 tests");

	test('Make sure thingy shows and hides - when we click button', function() {
		ok($('#thingy').is(':visible'), 'Thingy starts visible');

		$('#thingyButton').click();

		ok(!$('#thingy').is(':visible'), 'Thingy hidden after click');

		$('#thingyButton').click();

		ok($('#thingy').is(':visible'), 'Thingy visible again after second click');

		//go to page 2
		$$('#myFrame').attr('src', location.protocol + '//' + location.hostname + '/examples/qunit/page2.html');
	});
}

function page2tests() {
	module("Page 2 tests");

	test('Make sure ajax call works - when we click button', function() {
		ok($('#thingy').text() === 'Ajax response goes here', 'Text correct before ajax call');

		$('#ajaxButton').click();

		ok($('#thingy').text() === 'It worked!', 'Text correct after ajax call');

		//go to page 3
		$$('#myFrame').attr('src', location.protocol + '//' + location.hostname + '/examples/qunit/page3.html');
	});
}

function page3tests() {
	module("Page 3 tests");

	test('Make sure dialog comes up - when we click button', function() {

		ok(!$('#myDialog').is(':visible'), 'Dialog starts hidden');

		$('#dialogButton').click();

		ok(!$('#myDialog').is(':visible'), 'Dialog starts hidden');
	});
}

//load up a SOP friendly URL
$$('#myFrame').attr('src', location.protocol + '//' + location.hostname + '/examples/qunit/page1.html');

</script>

</html>

Note: We can (and probably should) actually move our tests into different .js files making it much easier to deal with as our number of tests grow.

We are doing a couple of interesting things here, first we are testing multiple pages at one time. Every time a new page loads up, we grab the jQuery refrence from inside of it, then run the tests that are specified for that page.

Another cool thing is how we get the jQuery reference for each page.

We start by loading jQuery using noConflict(true)

This is because we need to grab the jQuery instance from inside the actual website. When jQuery attaches events to DOM elements, it actually saves those events inside of itself, so although we can trigger events inside of the page itself using our external jQuery, triggering events inside of the iframe will not actually fire any events that were created inside the page. So, instead we grab the jQuery that is actually on the page and trigger the events using it. This has the added bonus that because it is the reference from inside the page we are loading, we can reference DOM nodes as though we were working that that page directly.

Here we grab the jQuery reference from inside the iframe with:

	$ = window.frames[0].jQuery;

Note: For ANY of this to work we must adhere to SOP, this is why we load this up on a server, side by side with our site. This is why I am loading up the iframe using location.protocol + ‘//’ + location.hostname rather than typing out the URL. This will allow it to work as we move between servers and such.

One of the cool things we are doing here is tying the starting of the tests to a button, so we can get the page to any state necessary before starting the tests – such as logging in, waiting for ajax requests to finish, or navigating to somewhere specific (not that we couldn’t do these in the tests, it just gives us options.)
Also, before we run our tests we are running the 2 following commands:

	$.ajax({async: false});
	$.fx.off = true;

This will make our ajax requests and animations synchronous. Although you would never do this (especially the ajax one) on a site, for our tests it is necessary to ensure that the page is ready before we perform each test.

Now that we have done this we can run our test just like we were on the page directly.

We can select elements and trigger events on them, like so:

	$(elem).click(); or $(elem).trigger(‘click’);
	$(elem).mousedown(); or $(elem).trigger(‘mousedown);
	Test things, such as visibility:
	$(elem).is(‘:visible’);

Basically anything you would normally do. Simple and easy.

This is not a perfect way to test your events, it is however pretty good, and will let us know when we break things.

Here are some examples of tests in action. I hope this helps you get started with unit testing.

Tests in action!

This entry was posted in javascript, jQuery, jQueryUI, programming, qUnit, Unit Testing. Bookmark the permalink.

4 Responses to Unit testing jQuery w/ QUnit

  1. eddy says:

    I think we’re on the same page. My goal was to test not only my one off functions but also my DOM manipulations. QUnit is kind of obtrusive for the latter, and I could have used something like funcunit but I wanted to keep it simple. So long story short, I created a script to log the results to the console. Just an alternative to using an iFrame.

    check it out if you’re still testing with QUnit, https://github.com/eddywashere/Qunit-Logging

    • RTPMatt says:

      Yeah man, that is cool. My goal is to leave the page as “untouched” as possible. You might be able to combine the two ideas. Running the unit tests does get a little annoying when you have to keep scrolling to the bottom of several hundred tests to see if they all passed.

  2. k7n4n5t3w4rt says:

    Hi Matt,
    I came across this post a couple of years ago when I googled something like “QUnit acceptance test iframe”. I knew I was onto something as I’d read your blog before and we were obviously thinking along the same lines.

    As others have obviously done ( http://goo.gl/D12sZa ), some other developers and I have been using QUnit in this way on quite a few sites. We’ve put the code up on GitHub if you’re interested:

    https://github.com/TheMonkeys/MonkeytestJS

    Thanks for the great posts. I’m going to put a reference to this page on the website we’ve put up for the project:

    http://monkeytestjs.io/

    Kynan

  3. Pingback: Professional Development 8/17 through 8/23/2015 | Code Ukemi

Leave a Reply

Your email address will not be published. Required fields are marked *