At work, I've been tasked with getting the team up to speed with TDD whilst we redevelop our website in PHP. I knocked together a presentation on the subject a coupla months ago, but before having a chance to present it, got shifted about in the internal dept structure for a month or so, and it kinda got temporarily shelved. I posted it online: "TDD presentation". I'm back on the PHP Team now, and need to update said presentation to be more work-requirement-specific, as well as cover unit testing our JavaScript. This has been on our agenda for a coupla years, but was never allowed to get any traction by the decision makers. Decision-making has improved now, so we're all go.
I have heard about Jasmine, and like the look of it, but have never actually downloaded / installed / ran it. I'm gonna do that today.
(Oh, blogging my work is not something I do... I'm actually off on sick leave at the moment - which I feel slightly guilty about - but I need to get this stuff done, so gonna do it today whilst I am unlikely to get interruptions. I figured as I'm doing it on my own time, I get to blog about it too ;-)
I am writing about this as I do it.
Jasmine
First up: Jasmine. This is what Wikipedia has to say about Jasmine:
Jasmine is an open source testing framework for JavaScript. It aims to run on any JavaScript-enabled platform, to not intrude on the application nor the IDE, and to have easy-to-read syntax. It is heavily influenced by other unit testing frameworks, such as ScrewUnit, JSSpec, JSpec, and RSpec.And from its own website:
Jasmine is a behavior-driven development framework for testing JavaScript code. It does not depend on any other JavaScript frameworks.
And a code sample from the same page:
describe("A suite", function() {
it("contains spec with an expectation", function() {
expect(true).toBe(true);
});
});
If you're familiar with TestBox (and if you're a CFML dev, you bloody should be!), then this will look comfortingly familiar. Indeed that code would run on TestBox. I know a bit about TestBox, so this is pleasing: I have a head start!
Download & Install
I'm gonna use 2.1.3, which is - at time of writing - the latest version of Jasmine. The download page is here: jasmine 2.1.3. I've D/Led that and unzipped it into a public directory.Running
This is too easy... it ships with a file SpecRunner.html, and browsing to that runs the tests. Here are the samples:Nice!
Example Code
Looking at the code within SpecRunner.html, we see this:<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Jasmine Spec Runner v2.1.3</title>
<link rel="shortcut icon" type="image/png" href="lib/jasmine-2.1.3/jasmine_favicon.png">
<link rel="stylesheet" href="lib/jasmine-2.1.3/jasmine.css">
<script src="lib/jasmine-2.1.3/jasmine.js"></script>
<script src="lib/jasmine-2.1.3/jasmine-html.js"></script>
<script src="lib/jasmine-2.1.3/boot.js"></script>
<!-- include source files here... -->
<script src="src/Player.js"></script>
<script src="src/Song.js"></script>
<!-- include spec files here... -->
<script src="spec/SpecHelper.js"></script>
<script src="spec/PlayerSpec.js"></script>
</head>
<body>
</body>
</html>
So that's all pretty straight forward. Include your code, then include the tests for said code. Done.
Here's the code being tested (Player.js):
function Player() {
}
Player.prototype.play = function(song) {
this.currentlyPlayingSong = song;
this.isPlaying = true;
};
Player.prototype.pause = function() {
this.isPlaying = false;
};
Player.prototype.resume = function() {
if (this.isPlaying) {
throw new Error("song is already playing");
}
this.isPlaying = true;
};
Player.prototype.makeFavorite = function() {
this.currentlyPlayingSong.persistFavoriteStatus(true);
};
And the tests (abridged from PlayerSpec.js):
describe("Player", function() {
var player;
var song;
beforeEach(function() {
player = new Player();
song = new Song();
});
it("should be able to play a Song", function() {
player.play(song);
expect(player.currentlyPlayingSong).toEqual(song);
//demonstrates use of custom matcher
expect(player).toBePlaying(song);
});
// [...]
//demonstrates use of expected exceptions
describe("#resume", function() {
it("should throw an exception if song is already playing", function() {
player.play(song);
expect(function() {
player.resume();
}).toThrowError("song is already playing");
});
});
});
Testable code
The important thing to note here is that to test the JS, the code needs to be presented in a testable way. Which means it needs to be written as a function, and callable. This might seem like stating the obvious, but how much of your JS code logic is basically inline callback / closures, implementing an event handler, eg:$("#someElement").on("click", function(event){
// lots of untestable logic here.
});
You can't test that code. Well you probably could, but not easily. What you need to do is not write code like that, instead, do what the Jasmine sample code above does: prototype-up an object, and stick your event handler functions in there, then call them as the event handler callback:
// SomeObject.js
function SomeObject() {
}
SomeObject.prototype.doTheClickThing = function(event) {
// lots of testable logic here.
};
// bindings.js
someObject = new SomeObject();
$("#someElement").on("click", someObject.doTheClickThing);
If you don't want to take an OO approach here, then at least stick your functions into library files, rather than have them inline.
Also don't write great wodges of procedural JS just in like allTheStuff.js. Break everything down into testable code, and then the JS you actually run when your HTML page loads should be simply binding that stuff to DOM elements, or just calling functions which then contain the logic. Apply the same logic you would in you CFML (etc) code: the CFM files (or better these days, your controllers) should contain a bare minimum of CFML, just enough to call the actual workhorse code which is nicely encapsulated inside CFCs. All your logic should be in CFCs. The only code in CFMs should be calling that CFC logic, and have a bare minimum of logic in and of themselves.
Giving it a go
I have some JS testing to do shortly... two of the entries in my "Something for the weekend? A wee code quiz (in CFML, PHP, anything really...)" are using JavaScript:So I'm gonna need to do my own implementation of this too. I'm gonna see what's involved in converting my CFML version into client-side JS, but in true TDD spirit, convert the tests first.
// SubseriesSpec.js
describe("Subseries", function(){
var subseries;
beforeEach(function() {
subseries = new Subseries();
});
describe("TDD tests", function(){
it("returns an array", function(){
var result = subseries.getSubseries([], 0);
expect(result).toEqual([]);
});
// [...]
it("returns a multi-element subseries", function(){
var threshold = 500;
var result = subseries.getSubseries([100,100], threshold);
expect(result.length).toBeGreaterThan(1);
});
it("total of elements should not be greater than threshold", function(){
var threshold = 500;
var result = subseries.getSubseries([100,100,100,100,100,100], threshold);
expect(subseries.sum(result)).not.toBeGreaterThan(threshold); // ie: !> => <=
});
// [...]
});
// [...]
});
This is an abridged version of the file (full listing is linked to at the top of the listing), showing just the key bits. the only really noteworthy bit is that there are matchers for testing equality, greater than, less than; but nothing for less than or equal to (or GTE). So instead I have used "not greater than". It's possible to write custom matchers, but that's an exercise for another day.
And now the function itself:
// Subseries.js
function Subseries() {
}
Subseries.prototype.getSubseries = function(series, threshold){
var working = [];
var sum = this.sum;
return series.reduce(function(reduction, current){
working.push(current)
while (sum(working) > threshold){
working.shift();
}
var workingIsBetterForLength = working.length > reduction.length
var workingIsBetterForTotal = working.length == reduction.length && sum(working) > sum(reduction)
return (workingIsBetterForLength || workingIsBetterForTotal) ? working.slice() : reduction
}, []);
};
Subseries.prototype.sum = function(array){
return array.reduce(function(carry,addend){
return carry + addend;
}, 0);
};
One thing that threw me here is needing to make a new reference to
this.sum
for use inside the reduction callback. this in that context seemed to be the window object. I don't quite get that (and am expecting someone to call me thick and suggest how to do it properly, forthwith).Plus I was surprised that there's no built-in method for summing an array in JavaScript? The more surprising thing is that having been using JS moderately frequently for a number of years, I had not actually noticed this until now! Still: it's easily solved with a quick reduction.
I'm going to do a different article on the similarities between the CFML and JS versions of this code, as there's really not much to it (surprisingly so!), but it's out of scope for this article.
Anyway, that all comes up roses:
Conclusion
All in all I dunno why I didn't start using Jasmine sooner. It's pretty bloody easy. We do have a requirement to run this stuff via a CI server, but I'll deal with that later. I see it runs on node.js, so there's an excuse to look at both at the same time. But... another day.Right. Me dodgy old eye is a bit tired now, so - like a nana - I'm off for a kip.
--
Adam