Jasmine - An Introduction
This page is the result of taking notes from the session about Jasmine by Davis W. Frank at this years Fluent Conference in San Francisco. Unfortunately I wasn’t there, but O’Reilly was so kind to give me access to the amazing video compilation of the sessions.
Intro
All the code samples of the talk are available in this Gist here. I’ve written them down here as well, so you might simply proceed to get through them.
Why to test?
Personally, I think this question shouldn’t be made any more nowadays, but still, that’s what Davis W. Frank mentioned in his talk:
- fix “broken windows”
- extract common behavior (without fear of breaking something)
- swap dependencies
- refactor as you need
Fear not! I am using Jasmine to refactor JavaScript safely
(by an unknown Twitter user)
Setup Jasmine and Useful links
Here are some useful links to get started:
- Jasmine GitHub repo: pivotal.github.com/jasmine
- Jasmine User Group
A first Jasmine Test: BallSpec.js
A first example of a Jasmine test:
describe("Ball", function () {
var ball;
before(function() {
ball = new Ball();
});
it("should start deflated", function() {
expect(ball.isFull()).toEqual(false);
});
});
The according Ball.js
looks like this
function Ball() {
var self = this;
var full = false;
self.inflate = function() {
full = true;
};
self.isFull = function() {
return full;
};
return self;
}
Matchers
Jasmine has the following “matchers”:
toBe()
toBeTruthy()
toBeDefined()
toMatch()
toContain()
toBeCloseTo()
toThrow()
- …
A detailled description can be found on the GitHub repo wiki.
Spies
..it also has Spies which is how they do test doubles in Jasmine, basically they include mocks, stubs, fakes or doubles.
Here’s an example of spying on something
describe("Game", function() {
var game, ball;
beforeEach(function(){
ball = new Ball();
spyOn(ball, "inflate").andCallThrough();
game = new Game();
});
});
The describes can even be nested (infinitely), so for instance
describe("Game", function(){
...
describe("with a not-full ball", function(){
beforeEach(function(){
game.prepare(ball);
});
it("should inflate before play", function(){
expect(ball.inflate).toHaveBeenCalled();
});
});
});
expect(ball.inflate).toHaveBeenCalled();
here verifies that the inflate function has actually been called. This is possible because before we added a spy on the inflate
function, remember:
spyOn(ball, "inflate").andCallThrough();
The Game.js
itself looks like
function Game() {
this.prepare = function(ball){
if(ball.isFull()){
return;
}
ball.inflate();
}
}
Spies Inspection
Another very powerful concept is the possibility to inspect the result of the execution of the spy, i.e. when spying on the “inflate” function like
spy(ball, "inflate");
it is then possible to inspect the call by using
ball.inflate.calls[0]
ball.inflate.mostRecentCall
ball.inflate.mostRecentCall.args[0]
and so on.
The Mock Clock
This mechanism is used to test calls to setTimeout()
or setInterval()
. Jasmine basically overwrites these functions and makes them synchronous s.t. they don’t tick until (in the test) one explicitly gives them the command to do so.
For example:
describe("Manually ticking the Jasmine Mock Clock", function() {
var timerCallback;
beforeEach(function() {
timerCallback = jasmine.createSpy('timerCallback');
jasmine.Clock.useMock();
});
it("causes a timeout to be called synchronously", function() {
setTimeout(function() {
timerCallback();
}, 100);
expect(timerCallback).not.toHaveBeenCalled();
jasmine.Clock.tick(101);
expect(timerCallback).toHaveBeenCalled();
});
});
Note the creation of the timer “mock” in the beforeEach
and then the explicit command jasmine.Clock.tick(101)
to tell the timers to tick.
Note also the line
expect(timerCallback).not.toHaveBeenCalled()
. Inside a chaining every expectation can be negated by adding the.not.
in the chain.
Jasmine Ajax
This is not part of the Jasmine core but is available on GitHub. It does a similar thing as the “Mock Clock” does with timers, just that it applies to ajax calls. It stubs the default xhr
object to avoid making real ajax calls as that’s not what we want when running automated (unit) tests.
For example:
describe("When using the Jasmine Ajax Mock", function() {
var onSuccess, onComplete, onFailure, request;
beforeEach(function() {
onSuccess = jasmine.createSpy('onSuccess');
onComplete = jasmine.createSpy('onComplete');
onFailure = jasmine.createSpy('onFailure');
jasmine.Ajax.useMock();
jQuery.ajax({
url: "example.com/someApi",
success: onSuccess,
complete: onComplete,
error: onFailure
});
request = mostRecentAjaxRequest();
});
it("prevents the call from leaving your system", function() {
expect(request.url).toEqual("http://example.com/someApi");
});
});
It is also possible to assert on a (here) successful response..
describe("When using the Jasmine Ajax Mock", function() {
var onSuccess, onComplete, onFailure, request;
...
describe("with a successful response", function() {
beforeEach(function() {
var successResponse = {
status: 200,
responseText: "w00t!"
};
request.response(successResponse);
});
it("should call the success callback", function() {
expect(onSuccess).toHaveBeenCalledWith("w00t!");
});
it("should call the complete callback", function() {
expect(onComplete).toHaveBeenCalled();
});
});
});
..or a failed response
describe("When using the Jasmine Ajax Mock", function() {
var onSuccess, onComplete, onFailure, request;
...
describe("with a successful response", function() {
beforeEach(function() {
var failureResponse = {
status: 500,
responseText: "Doh!"
};
request.response(failureResponse);
});
it("should call the failure callback", function() {
expect(onFailure).toHaveBeenCalledWith("Doh!");
});
it("should call the complete callback", function() {
expect(onComplete).toHaveBeenCalled();
});
})
});
It is important to understand that the ajax call is mocked out at the very last possible moment, therefore the response here that is is “stubbed” looks nearly like the one a server would send back down to the client.
var failureResponse = {
status: 500,
responseText: "Doh!"
};
Async
For ajax and the timeout functions the mocks should be used. Asyncs may be useful for dealing with nodejs for instance.
Q&A
Recommendations to test multiple browsers
Selenium web drivers might be an option, basically to run the tests and then to extract the results from the DOM.
How do you organize your files?
Dependent on project. Normally the tests folder tree structure should reflect the one of the source files. This is usually comfortable for easily finding the according source file and vice versa.