A couple of weeks ago, in a post about SQL unit testing, I mentioned that there are now several feature-rich JavaScript unit testing frameworks available. Perhaps the most popular, given its association with Angular, is Jasmine.
Jasmine’s main selling points (although it is free!), are described on its website as follows:
Jasmine is a behavior-driven development framework for testing JavaScript code. It does not depend on any other JavaScript frameworks. It does not require a DOM. And it has a clean, obvious syntax so that you can easily write tests.
I have found the transition to writing JavaScript unit tests with Jasmine relatively painless, and if you have experienced writing unit tests in a server-side language and you are planning to begin unit testing your JavaScript, you shouldn’t have any problems getting started.
Each of the following features of server-side unit testing frameworks, such as NUnit and JUnit, has an equivalent capability in Jasmine:
- Grouping related tests
- Assertions
- Setup and Teardown
In addition to these essential features, Jasmine includes mocking functionality, which has some similarities to server-side mocking frameworks such as Moq.
What Does a Jasmine Unit Test Look Like?
Whereas a tSQLt unit test is a stored procedure, and a NUnit test is a method, a Jasmine unit test is a function. To be more specific, a Jasmine unit test (or a ‘spec’ in Jasmine lingo) is defined by a call to the it
function. The it
function takes two parameters: a string
which represents the name of the test, and a function which defines the test itself. Here is a very simple example (all examples are on this plunker):
function getManagerName(){
return 'Roberto Martinez';
}
describe("getManagerName tests", function() {
it('returns correct value', function(){
var name = getManagerName();
expect(name).toBe('Roberto Martinez');
});
});
function getManagerName(){
return 'Roberto Martinez';
}
describe("getManagerName tests", function() {
it('returns correct value', function(){
var name = getManagerName();
expect(name).toBe('Roberto Martinez');
});
});
In this example, you can see that Jasmine tests follow the normal unit testing process: call a function, then perform an assertion to verify that the function behaves as expected. You will also see that the it
function call lives inside of a call to the describe
function.
We run our Jasmine tests by creating an empty html page in which we reference the JavaScript we are testing, our tests, the Jasmine library, and some CSS included in the Jasmine download (for instructions on downloading and running tests, visit the website). When this page loads, it automatically runs the tests, and displays the results in a nice-looking list, like this:
Grouping Related Tests
In a C# or Java application, we typically group related tests together in a single test class. For example, we may write a test class for each class in our application that we wish to test, or we may have a separate test class for each method we wish to test.
In Jasmine, we group related tests together using the describe
function. This function is used to create a test suite, that is, a collection of related tests. In our example above, our suite is given the name getManagerName
. A nice feature of Jasmine is that we can nest test suites, so for example, one describe
function call which contains all tests for a particular feature, can then contain a nested describe
function call for each of its individual functions.
Let’s build on our example to demonstrate this. Below we have an object literal which contains a number of functions. We will group all our tests for this object into a suite named Everton Object, and within this suite we will define sub-suites for each of its functions.
var evertonObject = {
getManagerName: function(){
return 'Roberto Martinez';
},
getYearFounded: function(){
return 1878;
},
getKitColor: function(kitType){
if(kitType==='home')
return 'blue';
else if(kitType==='away')
return 'black';
else
throw 'unknown kit type';
}
};
describe("Everton Object", function() {
describe("getManagerName", function() {
it('returns correct value', function(){
var name = evertonObject.getManagerName();
expect(name).toBe('Roberto Martinez');
});
});
describe("getYearFounded", function() {
});
describe("getKitColor", function() {
});
});
var evertonObject = {
getManagerName: function(){
return 'Roberto Martinez';
},
getYearFounded: function(){
return 1878;
},
getKitColor: function(kitType){
if(kitType==='home')
return 'blue';
else if(kitType==='away')
return 'black';
else
throw 'unknown kit type';
}
};
describe("Everton Object", function() {
describe("getManagerName", function() {
it('returns correct value', function(){
var name = evertonObject.getManagerName();
expect(name).toBe('Roberto Martinez');
});
});
describe("getYearFounded", function() {
});
describe("getKitColor", function() {
});
});
Assertions
We verify behaviour in Jasmine by calling the expect
function to assert that some condition is true
. The expect
function follows the typical Assert
pattern found in other testing frameworks, where we can compare values, but also can check whether or not objects exist, if exceptions have been thrown, if variables are “truthy”, and many other verifications.
Let's add some tests for a getKitColor
function, including a verification that an exception is thrown when appropriate.
var evertonObject = {
getManagerName: function(){
return 'Roberto Martinez';
},
getYearFounded: function(){
return 1878;
},
getKitColor: function(kitType){
if(kitType==='home')
return 'blue';
else if(kitType==='away')
return 'black';
else
throw 'unknown kit type';
}
};
describe("Everton Object", function() {
describe("getKitColor", function() {
it('returns blue for home', function(){
var color = evertonObject.getKitColor('home');
expect(color).toBe('blue');
});
it('returns black for away', function(){
var color = evertonObject.getKitColor('away');
expect(color).toBe('black');
});
it('throws exception for unknown kit type', function(){
var unknownKitType = function(){
evertonObject.getKitColor('good afternoon');
};
expect(unknownKitType).toThrow('unknown kit type');
});
});
);
var evertonObject = {
getManagerName: function(){
return 'Roberto Martinez';
},
getYearFounded: function(){
return 1878;
},
getKitColor: function(kitType){
if(kitType==='home')
return 'blue';
else if(kitType==='away')
return 'black';
else
throw 'unknown kit type';
}
};
describe("Everton Object", function() {
describe("getKitColor", function() {
it('returns blue for home', function(){
var color = evertonObject.getKitColor('home');
expect(color).toBe('blue');
});
it('returns black for away', function(){
var color = evertonObject.getKitColor('away');
expect(color).toBe('black');
});
it('throws exception for unknown kit type', function(){
var unknownKitType = function(){
evertonObject.getKitColor('good afternoon');
};
expect(unknownKitType).toThrow('unknown kit type');
});
});
);
Setup and Teardown
Setup
and Teardown
functions can be implemented in Jasmine by calling the beforeEach
, afterEach
, beforeAll
and afterAll
functions. These behave as you would expect.
describe("Everton Object", function() {
beforeEach(function() {
});
afterEach(function() {
});
});
describe("Everton Object", function() {
beforeEach(function() {
});
afterEach(function() {
});
});
Mocking
Jasmine uses spies to mock functions and therefore help us isolate pieces of code for testing. Spies let us easily count how many times a function is called, specify a return
value, or define an alternative implementation for a function. We need to call the spyOn
function to specify which functions we wish to track or mock.
Let’s add a getLeaguePosition
function which calls a getPosition
function on a separate object. Assume that this called function is quite complicated, and we therefore wish to mock it in order to isolate the function on evertonObject
. We will check that it is called, and also that the value it returns is returned by the function we are testing.
var evertonObject = {
getLeaguePosition: function(){
return leagueTable.getPosition();
}
};
var leagueTable = {
getPosition: function(teamName){
return 234;
}
};
describe("getLeaguePosition", function(){
beforeEach(function() {
spyOn(leagueTable, 'getPosition').and.returnValue(12);
});
it('calls leagueTable.getPosition', function(){
evertonObject.getLeaguePosition();
expect(leagueTable.getPosition).toHaveBeenCalled();
});
it('returns value from leagueTable.getPosition', function(){
var position = evertonObject.getLeaguePosition();
expect(position).toBe(12);
});
});
var evertonObject = {
getLeaguePosition: function(){
return leagueTable.getPosition();
}
};
var leagueTable = {
getPosition: function(teamName){
return 234;
}
};
describe("getLeaguePosition", function(){
beforeEach(function() {
spyOn(leagueTable, 'getPosition').and.returnValue(12);
});
it('calls leagueTable.getPosition', function(){
evertonObject.getLeaguePosition();
expect(leagueTable.getPosition).toHaveBeenCalled();
});
it('returns value from leagueTable.getPosition', function(){
var position = evertonObject.getLeaguePosition();
expect(position).toBe(12);
});
});
Disadvantages of Using Jasmine
It would seem fair to quickly outline some possible disadvantages of using Jasmine before finishing. Its main competitors in the JavaScript unit testing space are Mocha and QUnit. This article compares the three and suggests that testing asynchronous code can be troublesome, compared to Mocha. I am more than happy with Jasmine so far, although I am yet to test it with asynchronous requirements.
All examples here can be viewed and played with on this plunker.
The post Getting Started With Jasmine appeared first on The Proactive Programmer.