v2.0 API Developer Guide
Introducing Preamble
Preamble is a powerful JavaScript testing framework. Preamble runs in any modern HTML5 compliant browser as well as headless via PhantomJS and has no additional dependencies on any other libraries. Preamble is backed by a very powerful assertion engine that your test scripts interface with through a very simple to use but powerful API, which makes the task of authoring tests very easy, intuitive and fun.
This is an example of a simple synchronous test:
describe('truthy', function(){
it('true === true', function(){
isTrue(true);
});
});
And this is an example of a simple asynchronous test:
describe('Running asynchronous tests', function(){
var count = 0;
it('calling "done"', function(done){
setTimeout(function(){
count = 100;
done(function(){
equal(count, 100);
});
}, 1);
});
});
Installing Preamble
Whenever you want to create a new environment for creating and running tests just clone the repo into a folder on your computer and checkout the tagged version you are targeting (e.g. git checkout v1.3.0). That's it!
Run The Sample Test
After you have cloned the repo and checked out the tagged version you are targeting, you can then run the sample test script, javascripts/sample-test.js, by opening the index.html file in your browser. The index.html file is located in the repo's root folder.
Running a test script in the browser produces a report showing the results of the tests. All groups and tests are presented as links and when you click on them Preamble will run them again and display their details, respectively.
To repeat the test you can either refresh the browser or click on the run all link located near the top left corner of the page.
If you want to filter out passed test, check the Hide passed checkbox located near the top right corner of the page.
Once you have run the sample test script and familiarized yourself with the report you can then open up the sample script file in your editor and study the code to gain insight on writing your own test scripts.
index.html
The only required tags (other than the script tags) are <div id="preamble-test-container"></div> and <div id="preamble-ui-container"></div>.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Preamble</title>
<link href='stylesheets/preamble.css' rel='stylesheet' type='text/css'>
</head>
<body>
<!-- These are required. Do not remove them or rename their ids -->
<div id="preamble-test-container"></div>
<!-- v2.0.0 -->
<div id="preamble-ui-container"></div>
<!-- JavaScripts Go Here -->
<!-- Place script tags that your tests depend on here -->
<!-- The preamble-config.js file has to be loaded before preamble.js is loaded!!! -->
<!-- Note: You don't need to include this if you are using in-line configuration!!! -->
<!--
<script src="javascripts/preamble-config.js"></script>
-->
<!-- preamble.js -->
<script src="javascripts/preamble.js"></script>
<!-- Place your test script(s) here, immediately following preamble.js -->
<script src="javascripts/sample-test.js"></script>
</body>
</html>
API
When the windowGlobals configuration option is set to false the following API functions must be called as properties of the global Preamble object:
- describe - Preamble.describe
- it - Preamble.it
- beforeEach - Preamble.beforeEach
- afterEach - Preamble.afterEach
In addition to the above, when the windowGlobals configuration option is set to false test callback functions are passed a hash as their first parameter through which assertions must be called. It is common to name this parameter assert:
Preamble.it('this is a test', function(assert){
assert.equal(...);
assert.notEqual(...);
assert.isTrue(...);
assert.isFalse(...);
assert.isTruthy(...);
assert.isNotTruthy(...);
});
In the documentation that follows descriptions and code examples assume that the windowGlobals configuration option is set to true.
Grouping Tests
describe describe(label, callback)
describe provide structure and scope for one or more tests. label is a string used to uniquely identify the group. callback is a function which contains one or more tests. callback also provides scope to make data and code accessible to the tests.
describe('Describe a group', function(){
var hw = 'Hello World!';
it('Hello World!', function(){
isTrue(hw === 'Hello World!');
});
})
describe can also be nested providing fine grained structure for organizing tests:
describe('Nested specs', function(){
describe('Nested spec 1', function(){
it('test 1.1', function(){
isTrue(1);
});
});
describe('Nested spec 2', function(){
it('test 1.1', function(){
isTrue(1);
});
});
});
Tests
it it(label, [timeout,] callback([assert,] [done]){...})
it is used to define one or more assertions. label is a string used to uniquely identify a test within a group. timeout is an optional number used to override the default number of miliseconds Preamble waits before timing out a test (please see testTimeOutInterval in the Configuration section below for details). callback is a function which contains one or more assertions and it also provide scope to make data and code accessible to assertions.
assert is optional and is a hash that is alwyas passed as the first argument to it's and test's callback's when the configuration option windowGlobals is set to false. It exposes the assertion API. It is common to name this parameter assert.
Preamble.it('this is a test', function(assert){
assert.equal(...);
assert.notEqual(...);
assert.isTrue(...);
assert.isFalse(...);
assert.isTruthy(...);
assert.isNotTruthy(...);
});
done is optional and is a function that is passed as an argument to it's and test's callbacks and must be called to signal that an asynchronous process has completed. done's 'callback argument provides scope for one or more assertions.
describe('A test', function(){
it('Hello World!', function(){
var hw = 'Hello World!';
isTrue(hw === 'Hello World!');
});
});
describe('When running an asynchronous test', function(){
var count = 0;
it('calling done signals the asynchronous process has completed ', function(done){
setTimeout(function(){
count = 100;
done(function(){
equal(count, 100);
});
}, 1);
});
});
describe('When running an asynchronous test', function(){
var count = 0;
it('calling done signals the asynchronous process has completed ', 100, function(done){
setTimeout(function(){
count = 100;
done(function(){
equal(count, 100);
});
}, 50);
});
});
Setup and Teardown
beforeEach beforeEach(callback([done]){...})
afterEach afterEach(callback([done]){...})
beforeEach and afterEach are used to execute common code before and after each test, respectively. Their use enforces the DRY principle. callback provides scope for the code that is to be run before or after each test. Values can be passed on to tests by assigning them to callback's context (e.g. this.someValue = someOtherValue).
done is optional and is a function that is passed as an argument to the callbacks of beforeEach and afterEach and must be called to signal that an asynchronous setup/teardown process has completed.
describe('Using beforeEach to synchronously execute common code before each test', function(){
var count = 0;
beforeEachTest(function(){
count = 1;
});
it('Is count 1?', function(){
isFalse(count === 0, 'count doesn\'t equal 0');
isTrue(count === 1, 'count does equal 1');
isTrue((count += 1) === 2, 'count now equals 2');
});
it('Is count still 2?', function(){
isFalse(count === 2, 'nope, it isn\'t still 2');
isTrue(count === 1, 'now count equals 1');
});
});
describe('Using afterEach to synchronously execute common code after each test', function(){
var count = 0;
afterEachTest(function(){
count = 1;
});
it('Is count 0?', function(){
isTrue(count === 0, 'count does equal 0.');
});
it('Is count still 0?', function(){
isFalse(count === 0, 'count doesn\'t equal 0.');
isTrue(count === 1, 'count now equals 1.');
});
});
describe('Passing a value from Setup/Teardown on to a tests', function(){
beforeEach(function(){
this.value = 10;
});
it('the tests', function(){
equal(this.value, 10);
});
});
describe('Using beforeEach to asynchronously execute common code before each test', function(){
var count = 0;
beforeEach(function(done){
setTimeout(function(){
count = 10;
done();
}, 1);
});
it('beforeEach is called', function(){
equal(count, 10);
});
});
describe('Using afterEach to asynchronously execute common code after each test', function(){
var count = 0;
afterEach(function(done){
setTimeout(function(){
count = 1;
done();
}, 1);
});
it('the first asynchronous test', function(done){
setTimeout(function(){
count = 10;
done(function(){
isTrue(count === 10);
});
}, 1);
});
it('but subsequent asynchronous tests', function(done){
setTimeout(function(){
count *= 100;
done(function(){
isTrue(count === 100);
});
}, 1);
});
});
describe('Preventing a long running asynchronous Setup/Teardown from timing out a test', function(){
var count = 0;
beforeEachTest(function(done){
setTimeout(function(){
done(function(){
this.count = 10;
});
}, 50);
});
it('this.count should equal 10', 100, function(){
equal(this.count, 10);
});
});
Assertions
When the windowGlobals configuration option is set to false test callback functions are passed a hash as their first parameter through which assertions must be called. It is common to name this parameter assert:
Preamble.it('this is a test', function(assert){
assert.equal(...);
assert.notEqual(...);
assert.isTrue(...);
assert.isFalse(...);
assert.isTruthy(...);
assert.isNotTruthy(...);
});
equal equal(value, expectation, label)
A strict deep recursive comparison of value and expection. value and expectation can be any valid JavaScript primitive value or object (including functions). When comparing objects the comparison is made such that if value === expectation && expectation === value then the result will be true. label is a string used to uniquely identify the assertion.
notEqual notEqual(value, expectation, label)
A strict deep recursive comparison of value and expection. value and expectation can be any valid JavaScript primitive value or object (including functions). When comparing objects the comparison is made such that if value !== expectation && expectation !== value then the result will be true. label is a string used to uniquely identify the assertion.
isTrue isTrue(value, label)
A strict boolean assertion. Result is true if value is true. label is a string used to uniquely identify the assertion.
isFalse isFalse(value, label)
A strict boolean assertion. Result is true if value is false. label is a string used to uniquely identify the assertion.
isTruthy isTruthy(value, label)
A non strict boolean assertion. Result is true if value is truthy. label is a string used to uniquely identify the assertion.
isNotTruthy isNotTruthy(value, label)
A non strict boolean assertion. Result is true if value is not truthy. label is a string used to uniquely identify the assertion.
snoop
snoop snoop(obj, propName)
snoop is a utility that is used to spy on object methods. obj is the object whose method is to be spied on. propName is a string, its value is the name of the method to spy on.
snoop API
snoop provides a high level API for querying information about a method's invocation history:
wasCalled someObj.snoopedMethod.wasCalled()
Returns true if snoopedMethod was called, false if it wasn't called.
called someObj.snoopedMethod.called()
Returns the number of times that snoopedMethod was called.
wasCalled.nTimes someObj.snoopedMethod.wasCalled.nTimes(n)
Returns true if snoopedMethod was called n times.
contextCalledWith someObj.snoopedMethod.contextCalledWith()
Returns the context that snoopedMethod was called with.
args.getArgument someObj.snoopedMethod.args.getArgument(nth)
Returns the nth argument passed to snoopedMethod.
returned someObj.snoopedMethod.returned()
Returns what snoopedMethod returned.
//Snooping on an object's method
describe('snooping on a method', function(){
beforeEach(function(){
this.foo = {
someFn: function(arg){
return arg;
}
};
});
it('we can query if the method was called', function(){
var foo = this.foo;
snoop(foo, 'someFn');
foo.someFn();
isTrue(foo.someFn.wasCalled());
});
it('we can query how many times the method was called', function(){
var foo = this.foo;
snoop(foo, 'someFn');
foo.someFn();
equal(foo.someFn.called(), 1);
});
it('we can query the method was called n times', function(){
var foo = this.foo;
snoop(foo, 'someFn');
foo.someFn();
isTrue(foo.someFn.wasCalled.nTimes(1));
isFalse(foo.someFn.wasCalled.nTimes(2));
});
it('we can query the context the method was called with', function(){
var foo = this.foo,
bar = {};
snoop(foo, 'someFn');
foo.someFn();
equal(foo.someFn.contextCalledWith(), foo);
notEqual(foo.someFn.contextCalledWith(), bar);
});
it('we can query for the arguments that the method was called with', function(){
var foo = this.foo,
arg = 'Preamble rocks!';
snoop(foo, 'someFn');
foo.someFn(arg);
equal(foo.someFn.args.getArgument(0), arg);
notEqual(foo.someFn.args.getArgument(0), arg + '!');
isNotTruthy(foo.someFn.args.getArgument(1));
});
it('we can query for what the method returned', function(){
var foo = this.foo,
arg = 'Preamble rocks!';
snoop(foo, 'someFn');
foo.someFn(arg);
equal(foo.someFn.returned(), arg);
notEqual(foo.someFn.returned(), arg + '!');
});
});
//Snooping on multiple object methods
describe('snooping on more than one method', function(){
beforeEach(function(){
this.foo = {
someFn: function(arg){
return arg;
}
};
this.bar = {
someFn: function(arg){
return arg;
}
};
});
it('snoops are isolated and there are no side effects', function(){
var foo = this.foo,
bar = this.bar;
snoop(foo, 'someFn');
snoop(bar, 'someFn');
foo.someFn('Is Preamble great?');
bar.someFn('Yes it is!');
foo.someFn('You got that right!');
isTrue(foo.someFn.wasCalled());
isTrue(foo.someFn.wasCalled.nTimes(2));
isFalse(foo.someFn.wasCalled.nTimes(1));
isTrue(bar.someFn.wasCalled());
isTrue(bar.someFn.wasCalled.nTimes(1));
isFalse(bar.someFn.wasCalled.nTimes(2));
});
});
threw someObj.snoopedMethod.threw()
Returns true if snoopedMethod threw an exception.
threw.withMessage someObj.snoopedMethod.threw.withMessage()
Returns the message associated with the exception.
//Snooping if a method threw an exception and for the exception's message
describe('a snooped method throws', function(){
beforeEach(function(){
this.foo = {
someFn: function(){
throw new Error('Holy Batman!');
}
};
});
it('we can query if the method threw', function(){
var foo = this.foo;
snoop(foo, 'someFn');
foo.someFn();
isTrue(foo.someFn.threw());
isTrue(foo.someFn.threw.withMessage('Holy Batman!'));
isFalse(foo.someFn.threw.withMessage('Holy Batman!!'));
});
});
snoop.calls API
snoop.calls is a low level API that provides access to the accumulated information about a method's invocation history. Each invocation's information is stored in an ACall hash, and has the following properties:
context
The context used (its "this").
args
The arguments passed to the method.
error
If an exception was thrown when called this contains the exception's message.
returned
What the method returned.
snoop.calls API is defined as follows:
count calls.count()
Returns the total number of times the method was called.
forCall calls.forCall(n)
Returns a hash of information for the nth call to the method. The hash has the following properties:
all calls.all()
Returns an array of ACall hashes, one for each method invocation.
describe('using snoop\'s "calls" api', function(){
var i,
foo = {
someFn: function(arg){
return arg;
}
},
bar ={},
n = 3,
aCall;
snoop(foo, 'someFn');
for(i = 0; i < n; i++){
foo.someFn(i) ;
}
it('count() returns the right count', function(){
equal(foo.someFn.calls.count(), n);
});
it('all() returns an array with the right number of elements', function(){
equal(foo.someFn.calls.all().length, n);
});
it('forCall(n) returns the correct element', function(){
for(i = 0; i < n; i++){
aCall = foo.someFn.calls.forCall(i);
equal(aCall.context, foo);
notEqual(aCall.context, bar);
equal(aCall.args[0], i);
notEqual(aCall.args[0], n);
isNotTruthy(aCall.error);
equal(aCall.returned, i);
notEqual(aCall.returned, n);
}
});
});
UI Tests
Preamble adds the div element with the default id of ui-test-container to the DOM. Use of this element is reserved specifically for UI tests and Preamble itself never adds content to it nor does it ever modify its content. This element's ID can be overridden via configuration (please see Configuration below).
getUiTestContainerElement()
Returns the UI test container DOM element.
var uiTestContainerElement = getUiTestContainerElement();
getUiTestContainerElementId()
Returns the id of the UI test container DOM element.
var elUiTestContainerElement = document.getElementById(getUiTestContainerElementId());
Configuration Using preamble-config.js
The following configuration options can be overridden in the preamble-config.js file located in the javascripts folder:
windowGlobals
Default value = true. Set to false if you don't want to pollute the global name space and instead use the two global vars 'Preamble' and 'assert'.
testTimeOutInterval
Default value = 10 milliseconds. This is the value Preamble uses to wait before it times out a test. This value includes the time allocated to setup (beforeEach), teardown (afterEach) and the actual test (it or test).
name
Default value = 'Test'. Override this to display a meaningful name for your tests.
uiTestContainerId
Default value = 'ui-test-container'. Override this to use a different ID for the UI test container DOM element.
hidePassedTests
Default value = false. Set it to true to hide passed tests.
In-line Configuration
Begining with v2.0, you can call configure directly from within your test scripts.:
configure configure(hash)
Call configure passing a hash containing properties and their associated values for the configuration options to be overriden.
Place the call to configure at the very top of your test script file.
Please note that the windowGlobals configuration option can only be overriden by setting its value in the preamble-config.js configuration file and that it cannot be overriden using in-line configuration. Please see Configuration Using preamble-config.js above.
//Place the call to configure at the top of your test script file
configure({
name: 'Sample Test Suite',
hidePassedTests: true,
testTimeOutInterval: 100
});
.
.
.
Running Headless With PhantomJS
Please note that Preamble v2 requires PhantomJS v2.0.0 or better.
Please note that if you are installing the PhantomJS v2 binary distribution on a Mac you may need to follow the directions given here.
Beginning with v2 you can run headless tests with Preamble using PhantomJS v2. The following example assumes that you already have PhantomJS installed and that it can be found on the path.
- Open up a terminal and change to your test's root folder.
- From the command line enter "path/to/phantomjs javascripts/phantom-runner.js index.html".