Test spec¶
- Version:
- See buster-test
- Module:
require("buster-test").spec;
- In browsers:
buster.spec;
BDD-style specifications. Buster’s spec tests supports the same features as the xUnit-style test cases but with a syntax closer to BDD-style frameworks such as RSpec, Jasmine and others.
buster.spec.describe()
produces test context objects that can be run using the Test runner.
Unboxing the namespace¶
One of Buster’s core principles is to not pollute the global environment. For
this reason we only add one single global object – buster
. However, in
some cases, like this one, the namespaces seriously desugars your code. For
this reason, the spec
module has a buster.spec.expose()
method that
allows you to use
describe
,
it
,
itEventually
,
before
,
beforeAll
,
beforeEach
,
after
and
afterAll
,
afterEach
without the buster.spec
prefix:
buster.spec.expose();
describe("Namespace-less functions for ya", function () {
// ...
});
Describe¶
-
spec.
describe
(name, callback)¶ Creates a specification. The
name
should be a string, and thecallback
can be used to further describe your specification.
Example: Bowling kata¶
The following example shows some specs from the bowling kata, using a
before
method.
buster.spec.expose(); // Make functions global
var spec = describe("Bowling kata", function () {
before(function () {
this.game = new BowlingGame();
this.rollMany = function (rolls, pins) {
for (var i = 0; i < rolls; ++i) {
this.game.roll(pins);
}
};
});
it("yield 0 in score for gutter game", function () {
this.rollMany(20, 0);
buster.assert.equals(0, this.game.score());
});
it("yield score of 20 for 1 pin on each roll", function () {
this.rollMany(20, 1);
buster.assert.equals(20, this.game.score());
});
});
Example: controller specs¶
The following (slightly more involved) example shows some specs from a todo application’s form controller. Nested describes are used to separate both controller actions as well as successful and failed attempts at posting the form. Note the use of nested setup methods – both before callbacks will be run (the outer first, then the inner) for each requirement in the “adding items” specification.
buster.spec.expose();
var spec = describe("Form controller", function () {
before(function () {
this.form = document.createElement("form");
this.form.innerHTML = "<fieldset>" +
"<input type='text' name='item' id='item'>" +
"</fieldset>";
this.input = this.form.getElementsByTagName("input")[0];
this.backend = { add: sinon.spy() };
this.controller = todoList.formController.create(this.form, this.backend);
this.callback = sinon.spy();
this.controller.on('item', this.callback);
});
describe("adding items", function () {
before(function () {
this.input.value = "It puts the lotion in the basket";
});
describe( "successfully", function () {
it("emit onItem on success", function () {
var item = { text: "It puts the lotion in the basket" };
sinon.stub(this.backend, "add").yields(item);
this.controller.addItem();
sinon.assert.calledOnce(this.callback);
sinon.assert.calledWith(this.callback, item);
});
it("clear form on success", function () {
this.input.value = "It puts the lotion in the basket";
this.backend.add = sinon.stub().yields({});
this.controller.addItem();
buster.assert.equals("", this.input.value);
});
});
describe("unsuccessfully", function () {
it("render error on failure", function () {
sinon.stub(this.backend, "add").yields(null);
this.controller.addItem();
var err = this.form.firstChild;
buster.assert.match(err, {
tagName: "p",
className: "error",
innerHTML: "An error prevented the item from being saved"
});
});
});
});
});
Nested describes¶
Calls to describe
can be arbitrarily nested. See the explanation of
Nested before and after for an example of using nested describes.
Asynchronous specs¶
To create asynchronous specs (i.e. ones that the runner will wait for), the spec function can either explicitly accept a single argument, which is a function, or return a thenable promise.
Explicitly accepting an argument¶
The argument passed to the spec is a function. When the function is called, the asynchronous spec is deemed done. The idiomatic way of creating asynchronous specs using this arguments looks like the following:
buster.spec.expose();
describe("Buster async specs", function () {
it("be asynchronous", function (done) {
setTimeout(function () {
buster.assert(true);
done();
}, 100);
});
});
This assumes that the assertion framework can fail without throwing an error
(as an error would be intercepted as uncaught in the above example, if
intercepted at all). If this is not the case, you can make your assertions in a
callback to the done
function:
buster.spec.expose();
describe("Buster async specs", function () {
it("be asynchronous", function (done) {
setTimeout(function () {
done(function () {
buster.assert(true);
});
}, 100);
});
});
Returning a promise¶
Specs can be made asynchronous by way of returning a promise. The spec runner
considers any object with a then
method a promise:
buster.spec.expose();
describe("Buster async/promise specs", function () {
it("be asynchronous", function () {
var promise = {
then: function (callback) {
this.callbacks = this.callbacks || [];
this.callbacks.push(callback);
}
};
setTimeout(function () {
buster.assert(true);
var callbacks = promise.callbacks || [];
for (var i = 0, l = callbacks.length; i < l; ++i) {
callbacks[i]();
}
}, 100);
return promise;
});
});
Note that this does not work entirely as expected unless your assertion framework of choice is able to notify the runner of failure without throwing an exception. If the assertion fails (and throws an exception), the promise will never be resolved, thus the runner will fail the spec with a timeout, not an assertion error.
The above example is very verbose, simply to illustrate the duck-typed nature of promises. You can do better by using e.g. when.js:
describe("Buster async/promise specs", function () {
it("be asynchronous", function () {
var deferred = when.defer();
setTimeout(function () {
buster.assert(true);
deferred.resolver.resolve();
}, 100);
return deferred.promise;
});
});
Before and after callbacks can use the same mechanism to be asynchronous.
Before and after¶
Specs can use before
and after
callbacks. before
callbacks are
called before every spec, and is a suitable place to put shared setup code:
buster.spec.expose();
var spec = describe("Spec with before", function () {
before(function () {
this.object = { id: 42 };
});
it("override id": function () {
this.object.id = 43;
buster.assert.equals(this.object.id, 43);
});
it("not have id equal 43": function () {
// The object is recreated in setUp for each spec
buster.assert.notEquals(this.object.id, 43);
});
});
Similarly, after
callbacks can be used to clean up after each spec. Keep in
mind though, that the spec’s this
object is discarded and recreated for
each spec. If your specs are properly isolated you rarely need clean up.
buster.spec.expose();
var spec = describe("Spec with teardown", function () {
after(function () {
if (jQuery.ajax.restore) {
jQuery.ajax.restore();
}
});
it("make http request": function () {
twitter.timeline("cjno", function () {});
buster.assert(jQuery.ajax.calledOnce);
});
});
Using beforeAll() and afterAll()¶
Buster.js supports beforeAll()
and afterAll()
functions much like the
ones in Rspec. For example, if you want to run a setup function once and then
make the specs evaluate the result, you can do as follows:
function magicDoubler(number) {
return number * 2;
}
buster.spec.expose();
var spec = describe("The magic doubler", function () {
beforeAll(function() {
//magicDoubler is called only once.
this.result; = magicDoubler(7);
});
it("should yield a defined result", function () {
expect(this.result).toBeDefined();
});
it("should yield a number divisible by 2", function () {
expect(this.result % 2 === 0).toBeTrue();
});
});
Similarly, you can use afterAll()
to call a single teardown function that
runs after all specs have been executed. This is useful for cleaning up after
a test that alters a model.
Nested before and after¶
When nesting describes, you can add before
and after
callbacks to some
or all of your specs. All applicable before
and after
callbacks are
called before each spec function. before
callbacks are called starting from
the outermost describe
, while after
callbacks are called starting from
the spec’s local describe
. Let’s illustrate by way of an example:
buster.spec.expose();
var spec = describe("Nested before and after call order", function () {
before(function () {
console.log("Before #1");
});
after(function () {
console.log("After #1");
});
it("do #1", function () {
console.log("Spec #1");
});
describe("context", function () {
before(function () {
console.log("Before #2");
});
it("do #2", function () {
console.log("Spec #2");
});
describe("context", function () {
before(function () {
console.log("Before #3");
});
after(function () {
console.log("After #3");
});
it("do #3": function () {
console.log("Spec #3");
});
}
}
});
Will print:
Before #1
Spec #1
After #1
Before #1
Before #2
Spec #2
After #1
Before #1
Before #2
Before #3
Spec #3
After #3
After #1
Asynchronous before and after¶
Before and after callbacks are treated as asynchronous by the test runner if they either return a thenable promise or if they explicitly accept an argument. See Asynchronous specs.
Deferred specs¶
If you have written a spec that for some reason is impossible to pass in the near future, you may grow tired of seeing it fail while working on other parts of the system. Because the spec may represent an important goal/requirement (perhaps the goal of a longer refactoring session) it is undesirable to delete it. Simply commenting out the spec may cause you to forget it and commit commented out code, which isn’t very nice.
Buster recognizes the valid use of deferred specs and provides a simple way to
defer a spec – simply change it
to the aptly named itEventually
:
buster.spec.expose();
var spec = describe("Bowling kata", function () {
before(function () {
this.game = new BowlingGame();
this.rollMany = function (rolls, pins) {
for (var i = 0; i < rolls; ++i) {
this.game.roll(pins);
}
};
});
it("yield 0 in score for gutter game", function () {
this.rollMany(20, 0);
buster.assert.equals(0, this.game.score());
});
itEventually("yield score of 20 for 1 pin on each roll", function () {
this.rollMany(20, 1);
buster.assert.equals(20, this.game.score());
});
});
In this example, the second spec will not run, but the reporter will include it and explicitly mark it as deferred, helping you avoid forgetting about it.