Name
Test.More - A framework for writing test scripts
Synopsis
<pre id="test">
<script type="text/javascript">
plan({ tests: numTests });
// or
plan({ noPlan: true });
// or
plan({ skipAll: reason });
// Various ways to say "ok"
ok(got == expected, testDescription);
is(got, expected, testDescription);
isnt(got, expected, testDescription);
// Rather than document.write("# here's what went wrong\n")
diag("here's what went wrong");
like(got, /expected/, testDescription);
unlike(got, 'expected', testDescription);
cmpOK(got, '==', expected, testDescription);
isDeeply(complexStructure1, complexStructure2, testDescription);
if (!haveSomeFeature) skip(why, howMany);
else {
ok( foo(), testDescription );
is( foo(42), 23, testDescription );
}
TODO: {
todo(why, howMany);
ok( foo(), testDescription );
is( foo(42), 23, testDescription );
};
canOK(module, method);
isaOK(object, class);
pass(testDescription);
fail(testDescription);
isSet(gotArray, expectedArray, testDescription);
</script>
</pre>
Description
STOP! If you're just getting started writing tests, have a look at Test.Simple first. This is a drop in replacement for Test.Simple that you can switch to once you get the hang of basic testing.
The purpose of this module is to provide a wide range of testing utilities. Various ways to say "ok" with better diagnostics, facilities to skip tests, test future features and compare complicated data structures. While you can do almost anything with a simple ok()
function, it doesn't provide good diagnostic output.
I love it when a plan comes together
Before anything else, you need a an HTML element in which to run the tests, and you need a testing plan. The HTML element must have the ID "test" so that, when all test finish running, Test.More can check everything over and possibly output a test status message in the event that something has gone wrong.
A testing plan declares how many tests your script is going to run to protect against premature failure. The preferred way to create a testing plan is to use the plan()
function:
plan({ tests: numTests });
There are rare cases when you will not know beforehand how many tests your script is going to run. In this case, you can declare that you have no plan. (Try to avoid using this as it weakens your test.)
plan({ noPlan: true });
In some cases, you'll want to completely skip an entire testing script.
plan({ skipAll: reason });
Your script will declare a skip with the reason why you skipped and exit immediately with a zero (success).
You can also calculate the number of tests:
plan({ tests: someArray.length });
or to decide between running the tests at all:
if (!window.XMLHttpRequest) {
plan({ skipAall: 'Test irrelevant in IE' });
} else {
plan({ tests: 42 });
}
Test Descriptions
By convention, each test is assigned a number in order. This is largely done automatically. However, it's often very useful to assign a description to each test. Which would you rather see:
ok 4
not ok 5
ok 6
or
ok 4 - basic multi-variable
not ok 5 - simple exponential
ok 6 - force == mass * acceleration
The latter gives you some idea of what failed. It also makes it easier to find the test in your script: simply search for "simple exponential".
All test functions take a description argument. It's optional, but highly suggested that you use it.
I'm ok, you're not ok.
The basic purpose of this library is to print out either "ok #" or "not ok #" depending on whether a given test succeeded or failed. Everything else is just gravy.
All of the following functions print "ok" or "not ok" depending on if the test succeeded or failed. They all also return true or false, respectively.
- ok
-
ok(got == expected, testDescription);
This function simply evaluates any expression (
got == expected
is just a simple example) and uses its value to determine whether the test succeeded or failed. A true expression passes, a false one fails. Very simple.For example:
ok( exp{9} == 81, 'simple exponential' ); ok( p.tests == 4, 'saw tests' ); ok( items.length, 'items populated');
(Mnemonic: "This is ok.")
testDescription
is a very short description of the test to be printed out. It makes it very easy to find a test in your script when it fails and gives others an idea of your intentions.testDescription
is optional, but we very strongly encourage its use.Should an ok() fail, it will produce some diagnostics:
not ok 18 - sufficient mucus # Failed test 18 (foo.t at line 42)
This is actually the same as Test.Simple's ok() function.
- is
- isnt
-
is ( got, expected, testDescription ); isnt( got, expected, testDescription );
Similar to ok(), is() and isnt() compare the stringified value of their two arguments and use the result of that to determine if the test succeeded or failed. So these:
// Is the ultimate answer 42? is( ultimateAnswer(), 42, "Meaning of Life" ); // foo isn't empty isnt( foo, '', "Got some foo" );
are similar to these:
ok( ultimateAnswer() == 42, "Meaning of Life" ); ok( foo != '', "Got some foo" );
(Mnemonic: "This is that." "This isn't that.")
So why use these functions? They produce better diagnostics on failure than does ok(). ok() cannot know what you are testing for (beyond the name), but is() and isnt() know what the test was and why it failed. For example this test:
var foo = 'waffle', bar = 'yarblokos'; is( foo, bar, 'Is foo the same as bar?' );
Will produce something like this:
not ok 17 - Is foo the same as bar? # Failed test (foo.t at line 139) # got: 'waffle' # expected: 'yarblokos'
So you can figure out what went wrong without rerunning the test.
You are encouraged to use is() and isnt() over ok() where possible, however do not be tempted to use them to find out if something is
true
orfalse
!// XXX BAD! is( isFinite(brooklyn['trees']), true, 'Brooklyn has finite trees');
This example fails to check for whether
isFinite(brooklyn['trees'])
is true, it checks if it returnstrue
. Very different. Similar caveats exist forfalse
and 0. In these cases, use ok().is( isFinite(brooklyn['trees']), 'Brooklyn has finite trees');
- like
-
like( got, /expected/, testDescription );
Similar to ok(), like() matches this against the regex
/expected/
.So this:
like(got, /expected/, 'got is expected');
is similar to:
ok( /expected/.test(got), 'got is expected');
(Mnemonic "This is like that".)
The second argument is a regular expression. It may be given as a literal regex object (i.e.
//
ornew RegExp()
) or as a string that looks like a regex:like(got, 'expected', 'got is expected');
If <got> is not a string, the test will fail.
like([], /expected/, 'this will fail');
The advantages of like() over ok() are similar to that of is() and isnt(). Better diagnostics on failure.
- unlike
-
unlike( this, /that/, testDescription );
Works exactly as like(), only it checks if this does not match the given pattern.
- cmpOK
-
cmpOK( got, op, expected, testDescription );
Halfway between ok() and is() lies cmpOK(). This function allows you to compare two arguments using any binary JavaScript operator, plus a few Perl-style binary operators for string comparisons:
// ok( got.toString() == expected.toString() ); cmpOK( got, 'eq', expected, 'got eq expected' ); // ok( got == expected ); cmpOK( got, '==', expected, 'got == expected' ); // ok( got && expected ); cmpOK( got, '&&', expected, 'got && expected' ); // ...etc...
The advantage of cmpOK() over ok() is when the test fails you'll know what this and that were:
not ok 1 # Failed test (foo.html at line 12) # '23' # && # undef
It's also useful in those cases where you are comparing numbers and is()'s use of string comparison will interfere:
cmpOK( bigHairyNumber, '==', anotherBigHairyNumber );
The added perl operators are:
- eq
-
String equivalence.
- ne
-
String non-equivalence.
- lt
-
String less than.
- gt
-
String greater than.
- le
-
String less than or equal to.
- ge
-
String greater than or equal to.
- canOK
-
canOK(class, method, method, method...); canOK(object, method, method, method...);
Checks to make sure the class or object can do the defined methods.
canOK('Foo', 'foo', 'bar', 'whatever');
Handy for quickly testing an interface.
No matter how many methods you check, a single canOK() call counts as one test. If you desire otherwise, use:
for (var meth in ['foo', 'bar', 'whatever']) { canOK('Foo', meth); }
Note: Instances of classes defined by assigning anonymous objects to the
prototype
attribute of the constructor will not work properly in thecanOK()
function.My.Class = function () {}; My.Class.prototype = { foo: function () { return 'foo' }, bar: function () { return 'bar' } }; var my = new My.Class(); canOK(my, 'foo'); // Fail
This is because the anonymous object assigned to the prototype is constructed by the base
Object
class, not by theMy.Class()
construtor. The workaround is to callcanOK()
with the class name, instead:canOK('My.Class', 'foo'); // Pass
This is not an issue for prototypes that are simply assigned to:
My.Class = function () {}; My.Class.prototype.foo = function () { return 'foo' }; My.Class.prototype.bar = function () { return 'bar' }; var my = new My.Class(); canOK(my, 'foo'); // Pass
- isaOK
-
isaOK(object, class, objectName);
Checks to see if the given
object
is aclass
. Also checks to make sure the object was defined in the first place. Handy for this sort of thing:var obj = new SomeClass(); isaOK( obj, 'SomeClass' );
where you'd otherwise have to write
var obj = new SomeClass(); ok( defined obj && SomeClass.constructor.prototype.isPrototypeOf(obj));
to safeguard against your test script blowing up.
It works on core classes, too:
isaOK( [], 'Array' );
The diagnostics of this test normally just refer to 'the object'. If you'd like them to be more specific, you can supply an objectName (for example 'Test customer').
Note that inheritance is detected only for versions of JavaScript that support the isPrototypeOf() method. Earlier versions of JavaScript will only detect an exact match for class membership.
- pass
- fail
-
pass(testDescription); fail(testDescription);
Sometimes you just want to say that the tests have passed. Usually the case is you've got some complicated condition that is difficult to wedge into an ok(). In this case, you can simply use pass() (to declare the test ok) or fail (for not ok). They are synonyms for ok(1) and ok(0).
Use these functions very, very, very sparingly.
Diagnostics
If you pick the right test function, you'll usually get a good idea of what went wrong when it fails. But sometimes it doesn't work out that way. So here we have ways for you to write your own diagnostic messages.
- diag
-
diag(message); diag(message, message2, message3, ...);
Prints a diagnostic message that is guaranteed not to interfere with test output. All of the arguments to diag() will simply be concatinated together.
Handy for this sort of thing:
ok( users['foo']), "There's a foo user" ) || diag("Since there's no foo, check that /etc/bar is set up right");
which would produce:
not ok 42 - There's a foo user # Failed test (foo.html at line 52) # Since there's no foo, check that /etc/bar is set up right.
All diag()s can be made silent by passing the "noDiag" option to the plan() function:
plan({ tests: 1, noDiag: true });
. This is useful if you have diagnostics for personal testing but then wish to make them silent for release without commenting out each individual statement.
Conditional tests
Sometimes running a test under certain conditions will cause the test script to die. A certain function or method might not be implemented (such as window.XMLHttpRequest()
in Internet Explorer), some resource isn't available (like a net connection) or a JavaScript class isn't available. In these cases it's necessary to skip tests, or declare that they are supposed to fail but will work in the future (a todo test).
For more details on the mechanics of skip and todo tests see Test.Harness.
The way Test.More skips tests is with a named block. Basically, a block of tests that can be skipped over or made todo. It's best if I just show you...
- skip
-
if (condition) skip(why, howMany); else { // ...normal testing code goes here... }
This example demonstrates how to skip a series of tests,
howMany
tests to skip,why
and under whatcondition
to skip them. An example is the easiest way to illustrate:if (!window.XMLHttpRequest) skip("No XMLHttpRequest in your browser.", 1); else { isaOK(new XMLHttpRequest(), 'XMLHttpRequest'); }
If the user's browser doesn't support
window.XMLHttpRequest()
, we skip the test by callingskip()
. Test.More will output specialok
s that Test.Harness interprets as skipped, but passing, tests. If the browser does supportwindow.XMLHttpRequest()
, then obviously the tests will be run as normal.It's important that
howMany
accurately reflects the number of tests to skip so the number of tests run will match up with your plan. If your plan is "noPlan",howMany
is optional and will default to 1.Don't skip failing tests because there's a bug in your program, or for which you have not yet written code. For that you use todo(). Read on.
- todo
-
TODO: { todo(why, howMany); // ...normal testing code goes here... }
Declares a series of tests that you expect to fail and why. Perhaps it's because you haven't fixed a bug or haven't finished a new feature:
TODO: { todo("URIGeller not finished", 2); var card = "Eight of clubs"; is( URIGeller.yourCard(), card, 'Is THIS your card?' ); var spoon; URIGeller.bendSpoon(); is( spoon, 'bent', "Spoon bending, that's original" ); }
With todo(),
howMany
specifies how many tests are expected to fail. Test.More will run the tests normally, but print out special flags indicating they are "todo" tests. Test.Harness will interpret these failures as ok. Should any todo test pass, Test.Harness will report it as an unexpected success. You then know the thing you had todo is done and can remove the call to todo().The nice part about todo tests, as opposed to simply commenting out a block of tests, is that they're like a programmatic todo list. You know how much work is left to be done, you're aware of what bugs there are, and you'll know immediately when they're fixed.
It's a good idea to label a block that contains your TODO tests so that they're easy to find. Once a todo test starts succeeding, simply move it outside the and decrement the
howMany
argument totodo()
. When the block is empty, delete it. - todoSkip
-
if (condition) todoSkip(why, howMany); else { // ...normal testing code... }
With todo tests, it's best to have the tests actually run. That way you'll know when they start passing. But sometimes this isn't possible. Often a failing test will cause the whole program to die or hang. In such extreme cases you have no choice but to skip over the broken tests entirely.
The syntax and behavior of todoSkip() is similar to that of
skip()
except the tests will be marked as failing but todo. Test.Harness will interpret them as passing. - When do I use skip() vs. todo()?
-
If it's something the user might not be able to do, use skip(). This includes optional classes that aren't loaded, running in a browser that doesn't have some feature, or maybe running on a platform without an Internet connection.
If it's something the programmer hasn't yet done, use todo(). This feature is for any code you haven't written yet, or bugs you have yet to fix, but when want to test them, anyway (always a good idea).
- skipRest
-
if (condition) skipRest(why); // ...normal testing code
Sometimes you may have structured your tests so that you expect them all work up to a certain point, but perhaps not beyond that point. Use skipRest() at that point to skip all of the rest of the tests in the test file. This is effectively a shortcut for
skip(why, howMany)
, but you don't have to count how many tests to skip to get to the end of the test file.
Comparison functions
Not everything is a simple equality check or regular expression comparison. There are times you need to see if two arrays are equivalent, for instance. For these instances, Test.More provides a handful of useful functions.
- isDeeply
-
isDeeply( got, expected, testDescription );
Similar to is(), except that if
got
andexpected
are arrays or objects, it does a deep comparison, walking each data structure to see if they are equivalent. If the two structures are different, isDeeply() will output diagnostics identifying where they start differing. - isSet
-
isSet(gotArray, expectedArray, testDescription);
This function tests that two arrays have the same values, but unlike in isDeeply(), the order of the elements is not important. Only values are compared, not the keys (so it makes no difference whether you pass in a numeric array or an associative array). Note that this is a deep check, but the irrelevance of order only applies to the top level.
In the event of test failure, the diagnostic output will, as usual, indicate where the two sets differ. However, since the arrays are sorted, the values may not be in the same order (that is, with the same array indices) as they appeared in the arrays passed to isSet().
NOTE: By historical accident, this is not a true set comparison. While the order of elements does not matter, duplicate elements do matter.
Extending and Embedding Test.More
Sometimes the Test.More interface isn't quite enough. Fortunately, Test.More is built on top of Test.Builder, which provides a single, unified back end for any test library to use. This means two test libraries that both use Test.Builder can be used together in the same program.
If you simply want to do a little tweaking of how the tests behave, you can access the underlying Test.Builder object like so:
- builder
-
var testBuilder = Test.More.builder();
Returns the Test.Builder object underlying Test.More for you to play with.
- beginAsync
- endAsync
-
var timeout = 3000; var asyncID = beginAsync(timeout); window.setTimeout( function () { Test.ok(true, "Pass after 2 seconds"); endAsync(asyncID); }, timeout - 1000 );
Sometimes you may need to run tests in an asynchronous process. Such processes can be started using
window.setTimeout()
orwindow.setInterval()
in a browser, or by making an XMLHttpRequest call. In such cases, the tests might normally run after the test script has completed, and thus the summary message at the end of the test script will be incorrect--and the test results will appear after the summary.To get around this problem, use beginAsync() to prevent the test script from finishing until you pass the ID returned by beginAsync() to endAsync(). If you've called beginAsync() with the optional timout argument, then the test will finish if endAsync() has not been called with the appropriate ID before the timeout has elapsed. The timeout can be specified in milliseconds.
See Also
- Test.Simple
-
If all of these test functions confuse you and you just want to write some simple tests. You can upgrade to Test.More later (it's forward compatible).
- http://www.edwardh.com/jsunit/
-
JSUnit: elaborate xUnit-style testing framework.
Authors
Michael G Schwern <schwern@pobox.com> with much inspiration from Joshua Pritikin's Test module and lots of help from Barrie Slaymaker, Tony Bowden, blackstar.co.uk, chromatic, Fergal Daly and the perl-qa gang. JavaScript implementation by David Wheeler <david@kineticode.com>.
Copyright
Copyright 2001, 2002, 2004 by Michael G Schwern <schwern@pobox.com>, 2005 by David Wheeler.
This program is free software; you can redistribute it and/or modify it under the terms of the Perl Artistic License or the GNU GPL.
See http://www.perl.com/perl/misc/Artistic.html and http://www.gnu.org/copyleft/gpl.html.