Getting started
This tutorial covers getting a project set up, writing tests and running them with Node.js, before getting the tests running in a browser and on other server-side platforms.
We’ll start by creating a directory structure to host our new project:
project/
source/
test/
specs/
runner.js
vendor/
jsclass/
core.js
package.js
test.js
(etc)
JS.Test does not require any particular project layout, but I tend to lay
out my files like this. Our source directory contains the source code for
the project we’re building. The test/specs directory contains a spec file
for each source file; this is where we’ll be writing our tests. Finally,
test/runner.js is the script we’ll run from the command line or load into a
browser; its job is to load up all our code and tests and run the test suite.
Let’s add some code to runner.js. We need to load the JS.Class framework,
tell it where to find our tests, then run the test suite.
// test/runner.js
JSCLASS_PATH = 'vendor/jsclass';
require('../' + JSCLASS_PATH + '/loader');
JS.Packages(function() { with(this) {
autoload(/.*Spec$/, {from: 'test/specs'});
}});
JS.require('JS.Test', function() {
JS.require('UserSpec', JS.Test.method('autorun'));
});
That autoload statement tells JS.Packages it should look for any object
whose name matches /.*Spec$/ in the test/specs directory, for example
UserSpec should be in test/specs/user_spec.js. Finally we load JS.Test,
then load our specs and use the JS.Test.autorun() method to run them all.
Let’s try running it:

We’ve not created the spec file yet, so Node is complaining. Create a blank
file at test/specs/user_spec.js, then run the tests again:

This error happens because test/specs/user_spec.js doesn’t contain any code
yet: the next step is to start writing this spec.
Writing specs
Specs are organized using the nested context style popularized by RSpec.
Contexts are delimited using the describe method, and each test is created
using the it method. Within each context we can use before and after
hooks to set up and tear down any state the tests need. Assigning properties
to this in a before block makes them appear as local variables in the
tests.
Let’s write a simple spec for our User class.
// test/specs/user_spec.js
JS.ENV.UserSpec = JS.Test.describe('User', function() { with(this) {
before(function() { with(this) {
this.user = new User('James');
}});
it('has a name', function() { with(this) {
assertEqual('James', user.getName());
}});
}});
We need to make UserSpec a global variable so that JS.Packages can find
it. Accessing the global scope requires different code on different platforms,
but you can use JS.ENV to refer to it across all platforms.
If we run the test again we start to get meaningful output:

We’ve got two errors, one from the before block because the User class
doesn’t exist, and one from the test because the user variable was never
created. To fix this, we need to create the class, and tell JS.Packages
where to find it. Add this code to source/user.js:
// source/user.js
JS.ENV.User = new JS.Class('User');
Change test/runner.js to say that UserSpec requires User, and tell
it where to find the User class:
// test/runner.js
JSCLASS_PATH = 'vendor/jsclass';
require('../' + JSCLASS_PATH + '/loader');
JS.Packages(function() { with(this) {
autoload(/^(.*)Spec$/, {from: 'test/specs', require: '$1'});
file('source/user.js')
.provides('User')
.requires('JS.Class');
}});
JS.require('JS.Test', function() {
JS.require('UserSpec', JS.Test.method('autorun'));
});
Let’s run our tests again:

Just one error this time: our class doesn’t have the method we’re testing. Let’s finish writing our class so it passes the tests:
// source/user.js
JS.ENV.User = new JS.Class('User', {
initialize: function(name) {
this._name = name;
},
getName: function() {
return this._name;
}
});
One last test run:

Finally we’ve got a passing test. We can continue adding tests like this to build up our project. See the list of assertions below; they offer a rich set of tests to verify the behaviour of your code.
Running tests in the browser
To run our tests in a web browser we need a web page to host the tests, and we
need to separate the platform-specific script-loading code from the
platform-agnostic test-running code. Take the platform-specific code out of
test/runner.js and put it in test/console.js as shown below. We’re also
going to add a constant called ROOT to specify where the root of the project
is relative to the test page.
You should have two files that look like this:
// test/console.js
JSCLASS_PATH = 'vendor/jsclass';
require('../' + JSCLASS_PATH + '/loader');
require('./runner');
// test/runner.js
JS.Packages(function() { with(this) {
var ROOT = JS.ENV.ROOT || '.';
autoload(/^(.*)Spec$/, {from: ROOT + '/test/specs', require: '$1'});
file(ROOT + '/source/user.js')
.provides('User')
.requires('JS.Class');
}});
JS.require('JS.Test', function() {
JS.require('UserSpec', JS.Test.method('autorun'));
});
You should still be able to run the tests using node test/console.js in the
terminal. Now we just set up a web page that does the same job as
test/console.js, but in the browser:
<!-- test/browser.html -->
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Test runner</title>
</head>
<body>
<script type="text/javascript">ROOT = '..'</script>
<script type="text/javascript" src="../vendor/jsclass/loader.js"></script>
<script type="text/javascript" src="./runner.js"></script>
</body>
</html>
Save this as test/browser.html and open it in any browser to see the test
results.

When running in the browser, JS.Test will automatically notify TestSwarm
if you’re using it, so you can easily use JS.Test for your continuous
integration setup.
Running tests on other platforms
We’ve got tests that run in the browser and on the server using Node. But what happens if we run them with another tool, say Rhino?
~/project $ rhino test/console.js js: uncaught JavaScript runtime exception: ReferenceError: "require" is not defined.
Rhino (and other shells like V8 and SpiderMonkey) don’t support the require()
function to load files, they use load(). Let’s change the require() calls
in test/console.js to accommodate this:
// test/console.js
JSCLASS_PATH = 'vendor/jsclass';
if (typeof require === 'function') {
require('../' + JSCLASS_PATH + '/loader');
require('./runner');
} else {
load(JSCLASS_PATH + '/loader.js');
load('test/runner.js');
}
Now you should be able to run the tests on Rhino, V8, SpiderMonkey, Narwhal
and Node. You don’t need to change any other code; JS.require() works across
platforms so you only need to change the code that does the initial loading.
However, some platforms such as RingoJS don’t add variables assigned without
var to the global scope. JSCLASS_PATH must be a global variable but we
don’t yet have JS.ENV loaded to help us out. If you want to run on these
platforms, change your JSCLASS_PATH line to the following:
// test/console.js
(function() {
var $ = (typeof this.global === 'object') ? this.global : this;
$.JSCLASS_PATH = 'vendor/jsclass';
})();
Your tests will now run a wide range of server-side platforms, but not on
Windows Script Host. If you need to support this platform, you need to define
a load() function before doing anything else. Your final test/console.js
file will look as follows, at which point it will run on all supported
platforms:
// test/console.js
//================================================================
// Set up load() function for Windows Script Host
if (this.ActiveXObject)
load = function(path) {
var fso = new ActiveXObject('Scripting.FileSystemObject'),
file, runner;
try {
file = fso.OpenTextFile(path);
runner = function() { eval(file.ReadAll()) };
runner();
} finally {
try { if (file) file.Close() } catch (e) {}
}
};
//================================================================
// Set up JSCLASS_PATH variable
(function() {
var $ = (typeof this.global === 'object') ? this.global : this;
$.JSCLASS_PATH = 'vendor/jsclass';
})();
//================================================================
// Load the JS.Class package manager and test runner
if (typeof require === 'function') {
require('../' + JSCLASS_PATH + '/loader');
require('./runner');
} else {
load(JSCLASS_PATH + '/loader.js');
load('test/runner.js');
}