Test.AnotherWay: what's it?

It's a way to write tests for javascript code and html pages, run that tests and view results. It's modeled after Test::Simple from perl, like Test.Simple, but with prettier user interface. It is almost, but not quite, entirely unlike JsUnit. Test.AnotherWay main design goal is effortless setup and use. It has other features that may be of interest:

It should work in the browsers: IE 5.5 and 6.0, Mozilla/Firefox, Opera 7.5 and 8. Input events recording is not available in IE and versions of Opera prior to 8.0. IE 6 SP2 was not tested.

Back to its home page, with table of contents.

Setup

It consists of a single web page: run-tests.html. You open it in the browser, run tests and view results.
It looks like this (try it out here):

If you have run some tests here, you may note that results for successful test pages are collapsed, and results for failed test pages are expanded by default, making it easy to spot the exact place of failure. You can expand or collapse results of each test page by clicking on the name of a page in the results pane.

You should provide a list of pages that contain tests. There are three ways to do it:

The test page

Each test page should contain one or more test functions, with names beginning with test_. When you press the run buttons, the test page is loaded, and each test function in it is called. Each test function receives a parameter, test object, and you can call its methods to check the assertions and record the results. To illustrate this, here is contents of test-page-1.html from the example above:

<html><head><script type="text/javascript">
<!--
function test_1( t )
{
    t.plan( 1 ); // we will make one assertion
    t.ok( true, "this is ok" ); // that's it
}

// -->
</script></head><body></body></html>

Plans and assertions

Each test function can make any number of assertions. Quite naturally, if all the assertions hold true, the test pass. If any of the assertions is false, the test fails. A testing plan declares how many assertions your test function is going to make. This is required to detect premature exit from the test, and prevent situation when some of the assertions are left unchecked, causing the test to pass while in fact it fails. You tell how many assertions are in the test function by calling plan method:

    t.plan( 42 ); 

If you can't tell in advance how many assertions your test function is going to make, or if you are too lazy to count, you may omit the plan method call. This weakens your tests, so such tests are marked with "no plan" warning in the results pane.

You make assertions by calling the following methods of the test object: ok, fail, eq, like, html_eq. You can pass the descriptive string identifying the assertion as the last argument to each of those methods. It will be printed next to the result of the asserion in the results pane. This description argument is optional, but it is the only thing that identifies each assertion in a test function. So if it's omitted, and the test fails, someone will have a hard time trying to figure out which assertion in a function is to blame.

ok

The simplest assertion is ok. It takes two arguments: boolean value that gives the assertion result, and description.

    t.ok( items.length>1, "more than one item" ); 

fail

Sometimes you just know that the test must fail. In such cases, use the fail method. It takes single argument - a description.

    t.fail( "no good" ); 

eq

To test whether two values are equal, use the eq method. It takes three arguments: the value you want to test, the expected (known) value, and description.

    t.eq( 2+2, 4, "addition test" ); 

Values may be arbitrary data structures: arrays and objects nested to any level. Compound structures are considered equal if all their corresponding components (array elements, object members) are equal. This definition is recursive, and eq follows it naively, so if your data structure is circular (that is, it contains an object that refers to a part of that same structure), the eq method will blow up. Due to this, eq is unsuitable for comparing DOM trees - use html_eq instead.

If the values differ, the test fails and values are printed in the results pane. If the values are complex structures, diagnostics identifying where they start differing is printed.

like

The like method allows to check whether a string matches given regular expression. It takes three arguments: the string to test, the regular expression, and description.

    t.like( "15", /[\d]+/, "numbers match" ); 

html_eq

Use html_eq to test whether two chunks of HTML content is the same. html_eq takes three arguments: the HTML to test, the expected (known) HTML, and description. Each HTML value may be a DOM HTML node, or a string.

    t.html_eq( document.getElementById( "sample_id" ),
               "<div id=\"sample_id\" class=\"sample\">some text</div>",
               "compare sample_id div" ); 

html_eq ignores whitespace when doing comparison. It expects string HTML arguments to be well-formed, if they are not, the result of assertion will be of little use (inside html_eq, HTML strings are assigned to some innerHTML to convert them to nodes before comparison). If the HTML content differ, the test fails, and diagnostics pointing to the HTML fragment that differ is printed in the results pane. Also, the stringified HTML values are printed in the case of failure.

Open this page if you want to see the source and run an example test that uses each assertion mentioned above.

How to see what's going on

Sometimes it may be hard to figure out why some test fails. debug_print is primitive, but useful tool for tracing test execution and displaying variables. It takes one argument, a string that will be displayed in the run-tests.html page. Besides this string, it also will show the name of currently running test function and current time.

When the first debug_print call is made, two tabs appear on the run-tests.html page above the resuts pane, allowing to switch between debug messages and results. You can see the output of debug_print by running this test:

<html><head><script type="text/javascript">
<!--

function test_debug( t )
{
    t.plan( 1 );
    var items=[1,2];
    t.debug_print( items.toString() );
    t.ok( items.length>1, "more than one item" );
    t.debug_print( "bye" );
}

// -->
</script></head><body>
</body></html>

Testing asyncronous code

Suppose you want to test geocoding service - it should return latitude and longitude for any given street address, so that later it can be displayed on the map. Suppose also that this service uses asyncronous calling convention, returning results as an argument to callback function, in this way:

    geoCoords( address, function( lat, lng ) {  /* do something here */ } );

For illustration purposes, I will use completely bogus implementation of geoCoords just for this test:

function geoCoords( address, callback )
{
    var lat, lng;
    if( address.search( "305 Harrison St" )!=-1
     && address.search( "Seattle" )!=-1
     && address.search( "WA" )!=-1 ) {
        lat=46.620108;
        lng=-122.352333;
    }
    window.setTimeout( function() { callback( lat, lng ); }, 1000 );
}

Let's write the test for geoCoords in an obvious way:

function test_locate_obvious( t )
{
    t.plan( 1 );
    geoCoords( "305 Harrison St, Seattle, WA", function( lat, lng ) {
        t.eq( [46.620108, -122.352333], [lat, lng], "coordinates match" );
    } );
}

If you run it (in the frame below) it will fail, saying that it expected to see one assertion in the test function, but got none. The thing is, the results of test function are expected immediately after it returns, and assertion in the callback function is made too late. You can request that the counting of results be delayed for specified amount of time, allowing for such late assertions to be checked and recorded, by calling wait_result method:

    t.wait_result( 3 ); // seconds to wait 

After adding one line with wait_result call, the test becomes ok:

function test_locate_ok( t )
{
    t.plan( 1 );
    geoCoords( "305 Harrison St, Seattle, WA", function( lat, lng ) {
        t.eq( [46.620108, -122.352333], [lat, lng], "coordinates match" );
    } );
    t.wait_result( 1.5 );
}

Here is this example ready to run:

Waiting for things, in sequence

Another useful method for testing asyncronous code is delay_call. It takes any number of arguments, each of which may be a function or a delay (in seconds). Every function given to delay_call will be called, with the specified delay between subsequent calls. The default delay is 0.2 seconds. No arguments will be passed to each function.

delay_call is especially handy for testing web pages (see open_window below), when you initiate some action in web page and want to check that the page reacted accordingly. In some browsers, the time required for page objects to reflect the new state will be longer than the time required for javascript code to reach the assertion check. So if you try to check the state of the page objects immediately after the action, the test will fail. You may use the following pattern for introducing necessary delays:

    var button1=some_wnd.document.getElementById( "button1" );
    button1.click();
    t.delay_call( function() {
            /* check effects of button1 click here*/
            ...
            /* go on */
            var input1=some_wnd.document.getElementById( "input1" );
            input1.value="some value";
            var button2=some_wnd.document.getElementById( "button2" );
            button2.click();
        }, function() {
            /* check effects of button2 here */
        }
    );

Testing web pages

Suppose you have a web page that allows to use the above geocode service interactively. It has an input box for an address and a button, clicking on which displays latitude and longitude for given address. The source of this page is:

<html>
<head>
<title>Test.AnotherWay: interactive test page</title>
<script type="text/javascript">
<!--

function geoCoords( address, callback )
{
	var lat, lng;
	if( address.search( "305 Harrison St" )!=-1 && address.search( "Seattle" )!=-1 && address.search( "WA" )!=-1 ) {
		lat=46.620108;
		lng=-122.352333;
	}
	window.setTimeout( function() { callback( lat, lng ); }, 1000 );
}

function get_coords()
{
	var address=document.getElementById( "address" ).value;
	geoCoords( address, function( lat, lng ) {
		document.getElementById( "latitude" ).value=lat;
		document.getElementById( "longitude" ).value=lng;
	} );
}

// -->
</script>
</head>
<body><table><col width="8em">
<tr><td>address:</td><td><input type="text" id="address" /></td></tr>
<tr><td colspan="2">
<input type="button" value="get coords" onclick="get_coords();" id="get_coords" />
</td></tr>
<tr><td>latitude:</td><td><input type="text" id="latitude" /></td></tr>
<tr><td>longitude:</td><td><input type="text" id="longitude" /></td></tr>
</table></body>

Here is how it looks:

You can test this page using open_window method. It takes three arguments: url of a web page to open in a new window, a callback function to call when that window is opened, and optional timeout. If the window does not open before the timeout ends, the test fails. The default timeout is two seconds. Callback function receives one argument - window object for the new window. If the url for opened page has the same origin (protocol and host) as the run-tests.html, you can access document object of that window and initiate actions and query values of objects in that document.

Let's write a test for this interactive geocoding page (it is accessible from run-tests.html as doc/geo-input.html):

<html><head><script type="text/javascript">
<!--

function test_geo_input( t )
{
    t.plan( 2 );
    t.open_window( "doc/geo-input.html", function( wnd ) {
        var address=wnd.document.getElementById( "address" );
        address.value="305 Harrison St, Seattle, WA";
        var button=wnd.document.getElementById( "get_coords" );
        button.click();
        t.delay_call( 1.5, function() {
            var latitude=wnd.document.getElementById( "latitude" );
            t.eq( "46.620108", latitude.value, "latitude match" );
            var longitude=wnd.document.getElementById( "longitude" );
            t.eq( "-122.352333", longitude.value, "longitude match" );
        } );
    } );
}

// -->
</script></head><body>
</body></html>

Now you can run the test here:

Note that open_window may fail with misleading error message, if there is active popup blocker preventing scripts from opening windows. To run tests that use open_window you should disable popup blocker for the host where run-tests.html resides.

All windows opened by open_window are closed when the test finishes, unless you have checked the "do not close windows opened by tests" checkbox. This allows you to inspect what your test did to the windows, which may give some help in finding why the test failed.

You can simulate any action in the window opened by open_window, including clicking on links and moving back and forth in the history. Beware that window document object will be reloaded each time you initiate such an action, so there is no point in keeping references to that document or its subobjects across such actions. By the way, mozilla/firefox does not have a click method for links, but you can make your own that will work just fine, as shown in this article.

Simulating mouse input

Test.AnotherWay can record mouse input (movement and clicks) and replay it while the test is running, allowing to check whether the web page reacts to the user input properly.

Recording and replay does not work in Microsoft Internet Explorer and versions of Opera prior to 8.0. It is known to work in Firefox and Opera 8.0. Only mouse input is recorded, since no browser yet supports creation and dispatch of keyboard events in a DOM-compliant way. Recording works for single page only, if while recording you click on a link that opens another page, the recorder gets lost.

Recording

Recording is initiated from the same run-tests.html web page. You can choose the page for which the input will be recorded either by selecting it from the pull-down list of test pages, or by entering that page url, if it's not on the list. When you click "record" button on the run-tests.html, the selected page is opened, with yellow recording control box overlaid over it. The recording control indicates which object of the page has mouse over it, and lists available commands. All commands are triggered by the keyboard, by pressing Ctrl-Shift-letter combination.

You can start/stop recording by pressing Ctrl-Shift-s, pause/continue it by pressing Ctrl-Shift-p, and hide/show control box by Ctr-Shift-h.

By default, mousemove events are not recorded, since for many pages mouseover/mouseout events will suffice, and recording mousemove events causes the size of recorded data to grow immensely. You can enable/disable recording of mousemove events by Ctrl-Shift-m.

The only command not mentioned yet is Ctrl-Shift-c, with 'c' standing for 'checkpoint'. Now, what a checkpoint is good for requires some explanation.

The whole point of recording is to check that page reacts to the input properly. For that, you should specify two things: what to check, and when to perform those checks. The code that does the check may be written later, after the recording. But you should decide at which moments of time you will make those checks before you start the recording. When recording, you press Ctrl-Shift-c at those moments to insert checkpoints into the recorded sequence of events.

When you stop recording by pressing Ctrl-Shift-s, a new browser window automatically opens filled with javascript data structure. The structure looks like this:

{ checkpoints: [
 function( tst, wnd ) { // #0 time 3s. cursor was over HTML [0], BODY [2], UL [11], LI [1]
}, function( tst, wnd ) { // #1 time 8s. cursor was over I #i
}
], events: [
{target:"0 HTML,2 BODY", relatedTarget:"0 HTML,2 BODY,11 UL,5 LI", which:"19"....},
{target:"0 HTML,2 BODY,11 UL,5 LI", relatedTarget:"0 HTML,2 BODY", which:"19"....},
] }
;

It has two members: checkpoints and events. The latter is better left untouched. The checkpoints section contains empty functions, each function corresponds to one checkpoint. You should fill those functions with code asserting that the page indeed reacted properly to the input. Each checkpoint function receives two arguments: a test object suitable for calling its assertion methods, and window object with a page for which input events are replayed. To assist somehow in remembering what to test at which checkpoint, each function has a comment indicating the object on the page that had mouse over it, and the time in seconds from the beginning of the recording when the checkpoint was inserted.

To replay recorded events, you should copy that data structure, paste it into your test page, and pass it to the replay_events method.

replay_events

replay_events method takes two arguments: a window with web page for which to simulate events, and events data structure that was recorded earlier. Window should be opened by you prior to calling replay_event, possibly by open_window method. When replaying, the checkpoint functions written by you to make assertions about the page are called at appropriate moments. The arguments that each checkpoint function receives are: test object for which the replay_event was called, and the window object that was passed to it.

Let's do an example for replay_events, using it for testing how drag and drop works on this page:

This page is in doc/drag-drop.html file, and test for it is in doc/test-drag-drop.html.

It should work in this way: when you drag green rectangle over the blue one, the blue rectangle is highlighted with pink border, and when you drop the green rectangle over blue, it disappears, and the blue rectangle receives green border and its text changes to "done". You can see the full source of this page here (drag-drop is implemented in a quickly poor non-reusable way, good for this example only).

The input for this example is prerecorded, with mousemove recording turned on, and with three checkpoints: the first one before everything begins, the second one when the green rectangle is over the blue one, and the final one after the green rectangle is dropped. The source code of the test is:

<html><head><script type="text/javascript">
<!--

var drag_drop_events=
{ checkpoints: [
function( tst, wnd ) { // #0 time 1s. cursor was over DIV #record_control434
	var target=wnd.document.getElementById( "target" );
	tst.eq( "", target.style.border, "inital target border" );
	tst.like( /and drop here/, target.innerHTML, "initial target text" );

}, function( tst, wnd ) { // #1 time 5s. cursor was over DIV #object
	var target=wnd.document.getElementById( "target" );
	// browsers report border color in different ways
	tst.like( /3px solid (#cc0088)|(rgb\(204, 0, 136\))/, target.style.border,
		"object over target border" );

}, function( tst, wnd ) { // #2 time 9s. cursor was over DIV #target
	var object=wnd.document.getElementById( "object" );
	var target=wnd.document.getElementById( "target" );
	tst.eq( "none", object.style.display, "final object display" );
	tst.like( /3px solid (#008822)|(rgb\(0, 136, 34\))/, target.style.border,
		"final target border" );
	tst.eq( "done", target.innerHTML, "final target text" );
}
], events: [
{type:"_checkpoint", time:"1833", which:"0", target:"#record_control434 DIV"},
{which:"19", target:"#object DIV", type:"mouseover", ....},
// .... skipped a whole slew of lines like these. 
// View the source of doc/test-drag-drop.html for the completeness.
{which:"19", target:"#target DIV", type:"mousemove",.... }
] }
;

function test_drag_drop( t )
{
	t.plan( 6 );
	t.open_window( "doc/drag-drop.html", function( wnd ) {
		t.replay_events( wnd, drag_drop_events );
	} );
}

// -->
</script></head><body>
</body></html>

You can run it here:

Note that while the browser replays events, mouse cursor does not move. To visualize mouse movements while replaying, purple dot is drawn at the point where the mouse was when the test was recorded. After each call to checkpoint functions the dot flashes with green or red, depending on the outcome of the assertions in the checkpoint. Also note that if you move the mouse while the test is being replayed, it will interfere with replayed sequence of events.

Extra parameters for run-tests.html

run

If you want to run tests automatically immediately after the run-tests.html page is loaded, give it run parameter. It can take two forms: if you want to run all test pages, specify

run=all

Or if you want to run specific pages, give

run=page1.html,page2.html

Note that the page names given for run parameter should be spelled exactly in the same way (for example, with a path) as they appear in the testlist.html file or in testpage parameters.

testframe

By default, run-tests.html loads test page in a hidden iframe. Sometimes this can interfere with the test page workings, for example, in mozilla buttons ignore click() call when they are in the hidden iframe. To overcome this, you can open another instance of test page from itself by open_window, and perform tests on that instance. Such arrangement is not very convenient, so there is another way. You may load run-tests.html in a frame (or iframe) and tell it to use another visible frame for loading test pages. For this, you pass testframe query parameter to it:

run-tests.html?testframe=some_iframe_name

The value of testframe parameter should be a name of the frame for loading test pages, as accessible from top. The name may contain dots, if it is not a direct child of a top.

testframe_no_clear

This is handy in combination with testframe, it tells run-tests.html not to clear the test page frame when tests finish, so the last test page remain visible. It is useful when you want to inspect a test page and do something in it interactively.

run-tests.html?testframe=some_iframe_name;testframe_no_clear

jsantestpage

Same as testpage, but specifies that the test page adheres to Test.Simple test writing convention from jsan.

run-tests.html?jsantestpage=doc/jsan/one.t.html

You can also specify jsan calling convention for a page in the testlist.html file, by assigning "jsan" class name to its list item:

<ul id="testlist">
<li class="jsan">one.t.html</li>
<li class="jsan">two.t.html</li>
</ul>

See the file lib/Test/README.jsan and lib/Test/AnotherWay.js on how to run jsan module tests with Test.AnotherWay. Example tests written for jsan calling convention are in the doc/jsan subdirectory, you can run them by clicking here.