Jarvis

A JavaScript unit testing framework

Jarvis is a unit testing framework written in JavaScript. It’s based heavily off of the API of NUnit, which is a rewrite of a port of JUnit which is a port of SUnit. Eventually, there will be no new code: just rewrites of old code.

Download

Example

Here is a quick taste of what writing a test using Jarvis looks like:

// mytest.js
module.exports = (function(){
	var obj;

	return {
		setup: function() {
			obj = new MyClass();
		},

		tearDown: function() {
			obj.destruct();
			obj = null;
		},

		test: function My_test() {
			return [
				function Should_have_foo_property() {
					Assert.that(obj, Has.property('foo').equalTo('bar'));
				},

				function Should_be_valid() {
					Assert.that(obj.isValid(), Is.True());
				},

				function Should_be_ignored() {
					Assert.ignore('I am ignored');
				},

				function Should_catch_error() {
					Assert.willThrow();
					throw new Error();
				}
			];
		}
	};
}());
# command line
jarvis /path/to/mytest.js
// mytest.js
module.exports = (function(){
	var obj;

	return {
		setup: function(setupComplete) {
			obj = new MyClass();
			setupComplete();
		},

		tearDown: function(tearDownComplete) {
			obj.destruct(function() {
				obj = null;
				tearDownComplete();
			});
		},

		test: function My_test() {
			return [
				function Should_have_foo_property(testComplete) {
					Assert.begin()
						.that(obj, Has.property('foo').equalTo('bar'))
						.run(testComplete);
				},

				function Should_be_valid(testComplete) {
					Assert.begin()
						.that(obj.isValid(), Is.True())
						.run(testComplete);
				},

				function Should_be_ignored(testComplete) {
					testComplete({ ignore: 'I am ignored' });
				},

				function Should_catch_error(testComplete) {
					Assert.willThrow();
					testComplete(new Error());
				}
			];
		}
	};
}());
# command line
jarvis --async /path/to/mytest.js
function MyClass() {
	this.defaultName = "world";
	this.sayHello = function(name) {
		if (name === undefined) {
			name = this.defaultName;
		}

		if (typeof(name) !== "string") {
			throw "MyClass.bar() expects a string for the name";
		}

		return "Hello, " + name + "!";
	};

	this.appendToFoo = function(text) {
		document.getElementsByTagName("foo")[0].appendChild(document.createTextNode(text || ""));
	};
}

function getTests() {
	var myClass;

	return {
		setup: function() {
			myClass = new MyClass();
		},

		tearDown: function() {
			myClass = null;
		},

		test: function MyClass_tests() {
			return [
				function Default_name_should_be_world() {
					Assert.that(myClass.defaultName, Is.equalTo("world"));
				},

				function SayHello_should_emit_proper_punctuation() {
					Assert.that(myClass.sayHello(), Is.regexMatch(/^.+?,\s+.+!$/));
				},

				function How_about_a_sweet_looking_diff() {
					Assert.that(myClass.sayHello("good looking"), Is.equalTo("He'll do good on lookout"));
				},

				function SayHello_with_no_parameters_should_use_default_name() {
					Assert.that(myClass.sayHello(), Is.equalTo("Hello, world!"));
				},

				function Do_something_awesome() {
					Assert.ignore("This class doesn't do anything awesome, yet");
				},

				function Oops_an_error() {
					myClass.sayHello(20);
				},

				function SayHello_should_not_allow_non_strings() {
					Assert.willThrow("MyClass.bar() expects a string for the name");
					myClass.sayHello(20);
				},

				{
					setup: function() {
						document.body.appendChild(document.createElement("foo"));
					},

					tearDown: function() {
						document.body.removeChild(document.body.getElementsByTagName("foo")[0]);
					},

					test: function Tests_on_foo() {
						return [
							function Should_set_the_text_of_foo() {
								myClass.appendToFoo("this is the text");
								Assert.that("body > foo", Is.inDom());
								Assert.that("body > foo", Has.text.equalTo("this is the text"));
							},

							function Should_set_the_text_of_foo_to_empty_string() {
								myClass.appendToFoo();
								Assert.that("body > foo", Is.inDom());
								Assert.that("body > foo", Has.text.empty);
							}
						];
					}
				}
			];
		}
	};
}

//and run the tests
Jarvis.run(getTests());

Features

Usage

Jarvis tests are just simple functions that run whatever code you want. To run the test, pass the function to Jarvis.run(). That’s it.

If running tests using Node, there are several more options. You can still manually run tests using Jarvis.run(), but you can also run tests from the command line, which is probably preferred. After installing the npm module globally, run jarvis --help to view detailed options on how to use Jarvis from the command line.

Assertions are made in a slightly more verbose but more readable format. To assert something, make a call to Assert.that(actual, constraint). actual is the actual value for the comparison, and constraint is the return value of a call to one of the Is or Has functions. What you end up with is something that looks like this:

Assert.that(foo, Is.equalTo(bar));
Assert.that(foo, Has.property("length").greaterThan(1));

Jarvis will automatically use the name of the function as the name of the test, so it would behoove you to name your functions (as opposed to passing an anonymous function).

To create a test suite, have your test function return an array of test functions. See the example above for a concrete example. Jarvis will automatically handle child tests to an arbitrary depth.

To invoke a setup and/or tear down function before each test, pass an object to Jarvis.run(). If the object has a setup property, that will be the function that is run before each test. If the object has a tearDown (notice the capital D) property, that function will be run after each test. The actual test should reside in the test property.

API

The Jarvis object

Jarvis.run(function [, reporter])

Jarvis.run() is the function that does all the work of running the test. The first argument is a function reference. It also accepts an optional second argument to specify a reporter. If omitted, it defaults to Jarvis.defaultReporter.

Jarvis.summary([reporter])

Jarvis.summary() prints a summary of all tests that have been run so far using the given reporter.

Jarvis.defaultReporter

The defaultReporter property dictates how the results of the test are reported back to you, if not specified in the second argument of Jarvis.run(). Jarvis comes bundled with two reporters:

Jarvis.htmlDiffs

A boolean flag indicating whether to generate HTML diffs or not. Default is true.

Note: HTML diffs will only be generated when comparing two strings using Is.equalTo() or Is.identicalTo().

Jarvis.showStackTraces

A boolean flag indicating whether to show stack traces for errors and failures. Default is true.

This option is only relevant for the HtmlReporter on browsers other than Opera (Opera takes a long time to compute the stack trace so it’s always disabled).

Jarvis uses the stack trace library detailed here.

The Assert object

Assert.that(actual, constraint)

Assert.that() is the main entry point for making assertions.

actual is the value to compare against. constraint is the return value of a call to one of the Is or Has functions.

Assert.that("foo", Is.equalTo("foo"));
Assert.that("foobar", Is.not.regexMatch(/Foo/));

Assert.that([1, 2, 3], Has.value(2));
Assert.that({ foo: "bar" }, Has.key("foo"));
Assert.that({ foo: 7 }, Has.property("foo").greaterThan(5));
Assert.willThrow([errorObject])

Assert.willThrow() is equivalent to declaring an expected exception in other unit testing frameworks. If an error is raised after calling Assert.willThrow(), Jarvis will not report an error back. If an error is not raised, the test will fail.

If errorObject is given, Jarvis will verify that the thrown object is equal to errorObject.

Assert.ignore([message])

A call to Assert.ignore() ceases execution of the test. This will not be reported back as an error.

Assert.fail([message])

A call to Assert.fail() ceases execution of the test and is reported back as a failed test.

The Is object

Is.equalTo(expected)

This constraint evaluates for equality. What does equality mean? Well, it’s a little bit more relaxed to allow for more intuitive usage. The easiest way to figure out everything is to look at the tests of Jarvis itself. Yes, Jarvis uses Jarvis to test itself. There’s a yo dawg joke in here somewhere…

Here’s a summary of the non-obvious caveats:

Is.identicalTo(expected)

This constraint simply performs actual === expected.

Is.lessThan(expected)

This constraint simply performs actual < expected.

Is.lessThanOrEqualTo(expected)

This constraint simply performs actual <= expected.

Is.greaterThan(expected)

This constraint simply performs actual > expected.

Is.greaterThanOrEqualTo(expected)

This constraint simply performs actual >= expected.

Is.regexMatch(regex)

This constraint verifies that actual matches the given regex.

Assert.that("foo", Is.regexMatch(/foo bar/));
Assert.that("foo", Is.regexMatch(new RegExp("foo bar")));
Is.NULL()

This constraint simply performs actual === null.

Is.TRUE()

This constraint simply performs actual === true.

Is.FALSE()

This constraint simply performs actual === false.

Is.undefined()

This constraint simply performs actual === undefined.

Is.empty()

This constraint verifies that actual is empty. In Jarvis, an object is considered empty if it is:

Is.inDom()

This constraint verifies that the selector given by actual is present in the DOM.

Jarvis uses the Sizzle CSS selector engine, which is also the selector engine used by jQuery. That means any selector that you would pass to the jQuery object is valid.

Assert.that("#foo > input[type='text']", Is.inDom());
Is.not

This negates any constraint defined above by chaining them together. For example, Is.not.empty() will negate the empty constraint.

The Has object

Has is used for performing assertions on objects and arrays. It is a substitute for Is and is used in the same way.

Has.value(value)

This constraint verifies that the collection contains a value equal to the given value. Property names are ignored.

This constraint can be negated.

Assert.that([2, 4, 6], Has.value(4));
Assert.that([2, 4, 6], Has.no.value(3));
Has.key(key)

This constraint verifies that the collection contains a property with a name equal to the given key. Property values are ignored.

This constraint can be negated.

Assert.that([2, 4, 6], Has.key(0));
Assert.that({ foo: 1, bar: 2 }, Has.key("foo"));
Assert.that({ foo: 1, bar: 2 }, Has.no.key("qux"));
Has.property(name)

This function returns an interface identical to Is. It evaluates a constraint on the value of the property given by name.

This constraint cannot be negated.

Assert.that({ foo: 1, bar: 2 }, Has.property("foo").equalTo(1));
Has.text()

This property returns an interface identical to Is. It evaluates a constraint on the value of the text inside the DOM node identified by a selector.

This operates on the notion that the given selector matches an element whose first child is a DOM text node. Otherwise it will not match.

This constraint can be negated.

Assert.that("div > span.error", Has.text().equalTo("An error occurred"));
Has.flattenedText()

This property is identical to Has.text except that it will recursively go through each child node and append all text nodes.

For example, this assertion:

Assert.that("div > span.error", Has.flattenedText().equalTo("An error occurred"));

will pass for this HTML

<html>
	<body>
		<div>
			<span class="error">An <span>error <span>occurred</span></span></span>
		</div>
	</body>
</html>

This constraint can be negated.

ConsoleReporter

The ConsoleReporter reports test results to the console. This works on Firefox, Opera and Chrome. IE9’s console doesn’t support group so it just throws errors.

ConsoleReporter on Firefox (with Firebug):
firefox console

ConsoleReporter on Chrome:
chrome console

ConsoleReporter on Opera (with Dragonfly):
opera console

HtmlReporter

The HtmlReporter renders a color-coded interface. If a test has child tests, it will collapse them to save space. You can drill down into each child test by clicking on the test name.

The HtmlReporter constructor takes two optional arguments. The first argument is a DOM node to append the generated HTML to. If not given, it defaults to document.body. The second argument is an options object. The only option available currently is collapsedByDefault which defaults to true and will collapse all child tests by default.

An example of the HtmlReporter can be seen above or on Jarvis’ own test report.

CliReporter

The CliReporter renders reports similar to other xUnit CLI interfaces: a dot is a passed test, an "E" is an error, an "I" is an ignored test and an "F" is a failed test. An optional --verbose flag can be passed to the jarvis executable to display more information about each test.

Jarvis on the command line

About

Jarvis was written by Tommy Montgomery. You can reach him at tmont@tmont.com.

Jarvis makes use of the following open source libraries:

Report bugs or suggest enhancements either via email or on Github.