User Interfaces and Unittesting with the QUnit framework and jQuery

Just a while ago I checked out QUnit. I fiddled around with it, wrote some simple tests and I thought, this could be very useful. When writing code you always add some new features into existing functionality and that’s where the errors and regressions appear, especially when it gets complex.

As we know, unit testing is great for frameworks with a lot of different functions depending on each other (that’s one reason the jQuery-team developed or uses QUnit for testing). But how about testing user interfaces or simulating user interaction on websites? On websites we sometimes use small bits of code responding to the interaction or the input of a user, for example hiding or toggling elements or dynamicaly change content. When we modify parts of the code we don’t want to click every button to test if everything is responding normal.

There is one way to test parts of your interface without modifing your existing code (I don’t know if this is the best way but it’s one way to run tests in a simple way).

1. Setup your test environment

When you download QUnit, it comes with a basic test environment we can use. It looks something like that:

[...]
<link rel="stylesheet" href="../qunit/qunit.css" type="text/css" media="screen">
<script type="text/javascript" src="../qunit/qunit.js"></script>
[...]
<h1 id="qunit-header">QUnit Test Suite</h1>
<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</div>
[...]

2. Modify the test envoironment for our use

First, I added jQuery to our test envoironment, mainly to use the DOM ready provided by jQuery. Second, I added an iframe where the interface/website we want to test is placed:

[...]
<link rel="stylesheet" href="../qunit/qunit.css" type="text/css" media="screen"> 
<!-- add jQuery for the testing environment --> 
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="../qunit/qunit.js"></script> 
[...]
<h1 id="qunit-header">QUnit Test Suite</h1>
<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</div>
<iframe src="interface-we-want-to-test.html" height="240" width="320"></iframe>
[...]

3. Get the jQuery-Object from your original code

After the DOM ready we have two different jQuery objects in the browser. The one from the test environment and our original object in the iframe. To avoid any trouble we „cleanup“ the standard $ object in our test environment and get a new jQuery object in noConflict mode named „$q“ (for QUnit);

[...]
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="../qunit/qunit.js"></script>
<script type="text/javascript" charset="utf-8">
var $q = jQuery.noConflict(true),
$ = null,
jQuery = null;
</script>
[...]

4. Get the „original“ jQuery object

The next step is to get the jQuery object from the iframe to trigger events like clicks, hovers or submits in your interface. We can do this after the DOM ready of the test environment:

[...]
<div id="qunit-fixture">test markup</div>
<iframe src="interface-we-want-to-test.html" height="240" width="320"></iframe>
</body>
<script>
// on DOM ready
$q(function() {   // fire event when iframe is ready
    $q('#testframe').load(function() {
        // Get the jQuery Object from the original code
        $ = window.frames[0].jQuery;
    });
});
</script>
[...]

5. Let’s test

After the setup we can run a simple test. This is very basic…just an example.

[...]
// on DOM ready
$q(function() {   
    // fire event when iframe is ready   
    $q('#testframe').load(function() {      
        // Get the jQuery Object from the original code      
        $ = window.frames[0].jQuery;      
        test('check if dialog closes', 3, function() {         
            // On start this is visible         
            ok($('#dialog').is(':visible'));

            // Simulate a click         
            $('#close-dialog').click();         

            // Now #dialog should be hidden...         
            ok(!$('#dialog').is(':visible'));         

            // Click again         
            $('#close-dialog').click();         

            // #dialog should still be hidden...         
            ok(!$('#dialog').is(':visible'));      
        });   
    }); 
});
[...]

Though, this example isn’t very complex or logical but if we change (for some unknown reasons) our original code from:

$('#close-dialog').click(function() { $('#dialog').hide(); });

to

$('#close-dialog').click(function() { $('#dialog').toggle(); });

our test will fail, because after the second click the dialog would be visible again.

Some other test checking the error validation of a ajax form could look like this:

test('check if form throws error on email validation', function() {   
    // Write some invalid values to the email field   
    $('form input#email').val('somewrong.email.com');   

    // submit the form   
    $('form').submit();   

    // check if the input field gets an error   
    ok($('input#email').hasClass('error'));
}); 

Conclusion

I don’t know how complex the tests can get and I’m shure you can’t test everything but this is one aproach to test interface functions and avoid regressions without clicking through your interface and checking every response. Of course, this is just for testing the code and the response of the interface and not the usability. (This stuff was just tested in Google Chrome, no warranty for other browsers)

Download example or check the the GitHub Repository

Note: Make sure you run this on a server (or your local server) otherwise you will have problems accessing the iframe via JavaScript.

UPDATE:

  • 2012/02/25: Added a download link with a „working demo“ after Jesse Breuer made the proposal in the comments.

12 Kommentare Schreibe einen Kommentar

  1. Thank you for that update Michel. After downloading it, I moved the tests into the blank test/test.js file, and moved the link to that script after the iframe with this part at the top of the file: $q = jQuery.noConflict(true), $ = null, jQuery = null;That seems to be working fine for me as well.

  2. Great example, got me started with QUnit, much appreciated! Like the idea of capturing the frame’s jQuery object to run the tests.A couple of suggestions to anyone wanting to try this out. If your sources, tests, and test harness are in the local filesystem, in Chrome I ended up seeing "unsafe javascript attempt to access frame" when attempting to get the frame’s jQuery object from the test harness; Chrome treats local files as each in their own domain for security. The easiest workaround is to run Apache and have an alias or symlink to the directory and run the tests that way.Another improvement is to be able to write your tests (without any test harness setup) in their own javascript file, perhaps if you want to be able to reuse them headless, or in another test environment (such as jschilicat – http://jschilicat.chilicat.net/ ). However if you do this and include the test cases in a script tag at the bottom of the test harness, QUnit may start running them before the test fixture iframe is loaded and before the fixture’s jQuery is captured (hence the reason for including the tests in the onload handler). I figured out a relatively easy workaround for this too… override QUnit.begin straight after loading QUnit:QUnit.begin = function () { QUnit.config.autostart = false;};then end your onload handler with a call to QUnit.start(). Now your tests can be in their own file without having to be inside the document ready or the onload handler, but won’t run until those handlers complete.

  3. Thanks for sharing this simple but great post.I am wondering , will there be any limitations the full range of operations ( all AJAX operations etc. ) you can fire on the ‚main page‘ from the ‚ test.js ‚ file since the ‚main web page is in side IFRAME .instead of having the ‚actual web page‘ in IFrame , how about having a test/index.html ( test page) at the bottom of ‚actual web page‘ in IFRAME . Ofcourse you have to remove it once you finish te testing ( in your dev. environment ).

  4. Thank you for that update Michel. After downloading it, I moved the tests into the blank test/test.js file, and moved the link to that script after the iframe with this part at the top of the file: $q = jQuery.noConflict(true), $ = null, jQuery = null;That seems to be working fine for me as well.

  5. Great example, got me started with QUnit, much appreciated! Like the idea of capturing the frame’s jQuery object to run the tests.A couple of suggestions to anyone wanting to try this out. If your sources, tests, and test harness are in the local filesystem, in Chrome I ended up seeing "unsafe javascript attempt to access frame" when attempting to get the frame’s jQuery object from the test harness; Chrome treats local files as each in their own domain for security. The easiest workaround is to run Apache and have an alias or symlink to the directory and run the tests that way.Another improvement is to be able to write your tests (without any test harness setup) in their own javascript file, perhaps if you want to be able to reuse them headless, or in another test environment (such as jschilicat – http://jschilicat.chilicat.net/ ). However if you do this and include the test cases in a script tag at the bottom of the test harness, QUnit may start running them before the test fixture iframe is loaded and before the fixture’s jQuery is captured (hence the reason for including the tests in the onload handler). I figured out a relatively easy workaround for this too… override QUnit.begin straight after loading QUnit:QUnit.begin = function () { QUnit.config.autostart = false;};then end your onload handler with a call to QUnit.start(). Now your tests can be in their own file without having to be inside the document ready or the onload handler, but won’t run until those handlers complete.

  6. Thanks for sharing this simple but great post.I am wondering , will there be any limitations the full range of operations ( all AJAX operations etc. ) you can fire on the ‚main page‘ from the ‚ test.js ‚ file since the ‚main web page is in side IFRAME .instead of having the ‚actual web page‘ in IFrame , how about having a test/index.html ( test page) at the bottom of ‚actual web page‘ in IFRAME . Ofcourse you have to remove it once you finish te testing ( in your dev. environment ).

Schreibe einen Kommentar

Pflichtfelder sind mit * markiert.