JavaScript Testing with Jasmine
For years, JavaScript developers checked their code by amm uhmm - exactly, they didn't. The QA, if there was one, tested the overall UI end to end. However no one really checked the code as it was accustomed with server side languages.
Later testing frameworks started to emerge. QUnit pioneered the domain, which was followed by Jasmine and in the end Mocha appeared. All these framework matured with time and became standard in the industry. Nowadays, there is absolutely no excuse for JavaScript developer to write a code without writing the tests.
Which one?
As I've mentioned, currently there are three frameworks, which are mature enough to be considered by us. I personally prefer Jasmine over others, since it's already packaged with test double function (spy) and assertion framework and offers fairly headless running. For those who prefer configuring different aspects and implementations of your testing framework should look at Mocha. Moreover have a look at a comparison article with pros and cons of all networks, and decide what suits you best. The syntax in all of them is nearly the same, so you can easily migrate from one to another.
Jasmine
Jasmine is a behavior-driven development framework for testing JavaScript code. Your tests are separated into suits, which contains several tests, called specs. Think of suit as of test case. To overview the framework, we will be using the examples provided with the Jasmine distribution, using the latest at the time of writing - 2.0.0. Look at the structure of our folders. Our classes reside under folder named src. There we have two files Player and Song, which has only one method throwing an exception.
The next spec uses custom matcher declared in spec/SpecHelper.js, which performs the comparison based on several criterias.
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); };
function Song() { } Song.prototype.persistFavoriteStatus = function(value) { // something complicated throw new Error("not yet implemented"); };We'll start by looking at a first suit:
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); });We see here a test suit, called Player, in which we declare one spec using it function. Notice the beforeEach function - everything inside it is run before each spec is executed. The real magic happens when expect function is executed. Firstly we call play method of Player class, which is supposed to assign the property currentlyPlayingSong with the passed parameter. After that we check if it indeed does what is supposed by executing expect(player.currentlyPlayingSong).toEqual(song). It performs exactly what it is written - expects the passed parameter to be equal to the song variable. If the variables are not the same, exception is thrown.
The next spec uses custom matcher declared in spec/SpecHelper.js, which performs the comparison based on several criterias.
beforeEach(function () { jasmine.addMatchers({ toBePlaying: function () { return { compare: function (actual, expected) { var player = actual; return { pass: player.currentlyPlayingSong === expected && player.isPlaying } } }; } }); });Jasmine comes with lots of build-in matchers, so before you create your own, make sure it isn't already defined. Moving on to the second suit and being amazed by more matchers :)
describe("when song has been paused", function() { beforeEach(function() { player.play(song); player.pause(); }); it("should indicate that song is currently paused", function() { expect(player.isPlaying).toBeFalsy(); // demonstrates use of 'not' with a custom matcher expect(player).not.toBePlaying(song); }); it("should be possible to resume", function() { player.resume(); expect(player.isPlaying).toBeTruthy(); expect(player.currentlyPlayingSong).toEqual(song); }); });Since this suit is nested within the first one, the beforeEach is "added" to the one declared earlier. Thus player and song variables will be already defined. ToBeTruthy and ToBeFalsy are a bit tricky at first, both refer to anything that is considered true and false in JavaScript like null, 0 and undefined. Play around with them in specially created Jasmine cheat sheet.
it("checks if current song has been made favorite", function() { spyOn(song, 'persistFavoriteStatus'); player.play(song); player.makeFavorite(); expect(song.persistFavoriteStatus).toHaveBeenCalledWith(true); });In the beginning we tell Jasmine on which method we want to spy using spyOn function - persistFavoriteStatus method of instance song. Later we call some methods, which suppose to call the spied method and in the end we test if it was called and with which parameters.
We finish our presentation with the last suit, which demonstrates the ability to check for thrown exceptions using toThrowError function.
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"); }); });
This is not exception handling feature and shouldn't be used in your live code. Use try-catch instead.
Don't worry if still feel you haven't grasped the topic yet, I'll be writing more about JavaScript testing and Jasmine in particular. Today was just an introduction and overview of the concept and main features.
Comments
Post a Comment