testing javascriptwithjasmine sydjs
DESCRIPTION
Slides from my presentation on July 18 at Syd JSTRANSCRIPT
Testing JavaScriptLIKE A BOSS
Jo Cranford@jocranford
Thursday, 19 July 12
Y U NO JAVASCRIPT TESTS?
Thursday, 19 July 12
BDD With Jasmine Is Awesome Sauce
describe("Score Calculation Behaviour", function() {
it("should score 0 when no pins are knocked down", function() {
var game = new BowlingGame(10); game.roll(0);
expect(game.score()).toBe(0);
}); });
Thursday, 19 July 12
Is JavaScript Ever Really That Simple?
Thursday, 19 July 12
What About ...
• Asynchronous goodness
• Interacting with teh DOMz
• Evil Legacy Code
• Continuous Integration
• Clean readable tests that reflect your domain
Thursday, 19 July 12
Approaches To Testing Asynchronous Code
Thursday, 19 July 12
Let’s Load Some JSON
[ { "firstName": "Jo", "lastName": "Cranford", "company": "Atlassian" }, { "firstName": "Rachel", "lastName": "Laycock", "company": "ThoughtWorks" }]
Thursday, 19 July 12
The JavaScript Code
var Presentation = function() { this.presenters = [];};
Presentation.prototype.loadPresenters = function() { var presenters = this.presenters;
$.getJSON("people.json", function(data) { $.each(data, function(idx, person) { presenters.push(person); }); });};
Thursday, 19 July 12
Easy, Right?
describe("How not to test an asynchronous function", function () {
it("should load the presenters", function () {
var presentation = new Presentation(); presentation.loadPresenters();
expect(presentation.presenters.length).toBe(2);
});});
Thursday, 19 July 12
Well ... Not So Much.
Thursday, 19 July 12
But This Might Work ...
describe("Still not ideal though", function () {
it("should load the presenters", function () {
spyOn($, "getJSON").andCallFake(function (url, callback) { callback([{},{}]); })
var presentation = new Presentation(); presentation.loadPresenters();
expect(presentation.presenters.length).toBe(2);
});});
Thursday, 19 July 12
A Little Detour ...
Thursday, 19 July 12
Spy On An Existing Method
it("can spy on an existing method", function() {
var fakeElement = $("<div style='display:none'></div>"); spyOn(fakeElement, 'show');
var toggleable = new Toggleable(fakeElement);
toggleable.toggle();
expect(fakeElement.show).toHaveBeenCalled();
});
Thursday, 19 July 12
Spy On An Existing Method
it("can create a method for you", function() {
var fakeElement = {}; fakeElement.css = function() {}; fakeElement.show = jasmine.createSpy("Show spy");
var toggleable = new Toggleable(fakeElement);
toggleable.toggle();
expect(fakeElement.show).toHaveBeenCalled();
});
Thursday, 19 July 12
Wait, There’s More ...
• expect(spy).not.toHaveBeenCalled()
• createSpy().andReturn(something)
• createSpy().andCallFake(function() {})
• createSpy().andCallThrough()
Thursday, 19 July 12
Spy On The Details
• expect(spy).toHaveBeenCalled()
• expect(spy.callCount).toBe(x)
• expect(spy).toHaveBeenCalledWith()
• Tip: use jasmine.any(Function/Object) for parameters you don’t care about
Thursday, 19 July 12
... And We’re Back.
Sooooo ... spies are great and all, but what if your callback function
takes a while to run?
Thursday, 19 July 12
Don’t Do This At Home.
Presentation.prototype.loadPresentersMoreSlowly = function() {
var preso = this;
$.getJSON("people.json", function(data) { setTimeout(function() { $.each(data, function(idx, person) { preso.presenters.push(person); }); }, 2000); });
};
Thursday, 19 July 12
Don’t Do This, Either.it("should have loaded after three seconds, right?", function() {
spyOn($, "getJSON").andCallFake(function(url, callback) { callback([{}, {}]); })
var presentation = new Presentation(); presentation.loadPresentersMoreSlowly();
setTimeout(function() { expect(presentation.presenters.length).toBe(2); }, 3000);
});
Thursday, 19 July 12
But What If I Just ...
Presentation.prototype.loadPresentersMoreSlowly = function() {
var preso = this;
$.getJSON("people.json", function(data) { setTimeout(function() { $.each(data, function(idx, person) { preso.presenters.push(person); }); preso.presentersHaveLoaded = true; }, 2000); });
};
Thursday, 19 July 12
Now Wait, Wait ... RUN!it("should load the presenters", function() {
spyOn($, "getJSON").andCallFake(function(url, callback) { callback([{}, {}]); })
var presentation = new Presentation(); presentation.loadPresentersMoreSlowly();
waitsFor(function() { return presentation.presentersHaveLoaded; }, "presenters have loaded");
runs(function() { expect(presentation.presenters.length).toBe(2); });
});
Thursday, 19 July 12
Testing Interaction With The DOM
• Do you REALLY need to?
• Tests will have a high maintenance cost
• Instead separate logic from view and test logic
• Use templates for the view
Thursday, 19 July 12
Testing Interaction With The DOM
it("should display the score", function() {
setFixtures("<div id='score'></div>");
var bowlingGameView = new BowlingGameView(); bowlingGameView.showScore(100);
expect($("#score").text()).toBe("Your current score is 100");
});
https://github.com/velesin/jasmine-jqueryThursday, 19 July 12
Legacy (untested) JavaScript Code
• Long methods
• Violation of Single Responsibility Principle
• Side effects
• Lack of dependency injection
• Lots of new X()
• Unclear intentions
Thursday, 19 July 12
Testing Interaction
it("should call the method on the dependency", function() {
var dependency = {}; dependency.method = jasmine.createSpy();
var myObject = new Something(dependency); myObject.doSomething();
expect(dependency.method).toHaveBeenCalled();});
Thursday, 19 July 12
If Dependencies Aren’t Injected ...
var LegacySomething = function() {
this.doSomething = function() { var dependency = new Dependency(); dependency.method(); };
};
Thursday, 19 July 12
Create Stubs
it("is a pain but not impossible", function() {
Dependency = function() {}; Dependency.prototype.method = jasmine.createSpy()
var myObject = new LegacySomething(); myObject.doSomething();
expect(Dependency.prototype.method).toHaveBeenCalled();});
Thursday, 19 July 12
Continuous Integration
• Ruby Gem
• Maven
• Node.js
• Rhino (Java)
Thursday, 19 July 12
Ruby Gem
> rake jasmine:ci
require 'jasmine'load 'jasmine/tasks/jasmine.rake'
https://github.com/pivotal/jasmine-gemThursday, 19 July 12
Maven
> mvn clean test
http://searls.github.com/jasmine-maven-plugin/Thursday, 19 July 12
Node.js
> jasmine-node specs/
https://github.com/mhevery/jasmine-nodeThursday, 19 July 12
Rhino
• Download:
• Rhino (js.jar) from Mozilla
• env.rhino.js from www.envjs.com
• Jasmine console reporter from Larry Myers Jasmine Reporters project (github)
http://www.build-doctor.com/2010/12/08/javascript-bdd-jasmine/Thursday, 19 July 12
Rhinoload('env.rhino.1.2.js');
Envjs.scriptTypes['text/javascript'] = true;
var specFile;
for (i = 0; i < arguments.length; i++) { specFile = arguments[i];
console.log("Loading: " + specFile);
window.location = specFile}
> java -jar js.jar -opt -1 env.bootstrap.js ../SpecRunner.html
Thursday, 19 July 12
Extending Jasmine With Custom Matchers
it("should match the latitude and longitude", function() {
var pointOnMap = { latitude: "51.23", longitude: "-10.14" };
expect(pointOnMap.latitude).toBe("51.23"); expect(pointOnMap.longitude).toBe("-10.14");
});
it("should match the latitude and longitude", function() {
var pointOnMap = { latitude: "51.23", longitude: "-10.14" };
expect(pointOnMap).toHaveLatitude("51.23"); expect(pointOnMap).toHaveLongitude("-10.14");
});
Thursday, 19 July 12
Extending Jasmine With Custom Matchers
it("should match the latitude and longitude", function() {
var pointOnMap = { latitude: "51.23", longitude: "-10.14" };
expect(pointOnMap).toHaveLatLongCoordinates("51.23", "-10.14");
});
Thursday, 19 July 12
Extending Jasmine With Custom Matchers
beforeEach(function() { this.addMatchers({ toHaveLatitude: function(lat) { return this.actual.latitude === lat; }, toHaveLongitude: function(lat) { return this.actual.latitude === lat; }, toHaveLatLongCoordinates: function(lat, lng) { return (this.actual.latitude === lat &&
this.actual.longitude === lng); } });});
Thursday, 19 July 12
Custom Failure Messages
toHaveLatitude: function(lat) { this.message = function() { return "Expected Latitude " + this.actual.latitude
+ " to be " + lat; }; return this.actual.latitude === lat;}
Thursday, 19 July 12
Thursday, 19 July 12