Sunday, 19 May 2013

Testing JavaScript with Jasmine

When developing a web application, sooner or later we need to deal with JavaScript. To keep high code quality we must write unit tests.
Jasmine is a nice testing framework that allows us to write tests in a BDD manner.
Let's see how to use it in our project.

First of all, we need to incorporate it into our build. There is a maven plugin that will let us execute JavaScript tests within test build phase. We will add it to our pom.xml :
 
<plugin>
<groupId>com.github.searls</groupId>
<artifactId>jasmine-maven-plugin</artifactId>
<version>1.3.1.2</version>
<executions>
<execution>
<goals>
<goal>test</goal>
</goals>
</execution>
</executions>
</plugin>

Apart from executing tests within build, we can run them during development phase.
The maven goal jasmine:bdd starts the Jetty server and under http://localhost:8234 URL we can see the results of executing test fixtures.
Reloading the page will execute the latest version of our code.

Let's see it in action. We have a simple JavaScript function residing in src/main/javascript/simple.js that increments the number passed as an argument.

var increment = function (number) {
if (isNaN(number)) {
throw 'Argument ' + number + ' is not a number'
}
return number + 1
}
incrementer = {
increment: increment
}
view raw incrementer.js hosted with ❤ by GitHub
The test (in Jasmine called spec) is located in src/test/javascript/simple_spec.js:

describe('incrementer tests', function() {
it ('should be defined', function() {
expect(incrementer).toBeDefined()
})
it ('should increment', function() {
expect(incrementer.increment(1)).toEqual(2)
})
it ('should throw exception when argument is not a number', function() {
var illegalArg = 'stringy'
var thrown
try {
incrementer.increment(illegalArg)
} catch (e) {
thrown = e
}
expect(thrown).toBe('Argument ' + illegalArg + ' is not a number')
})
})
A test begins with a call to the global Jasmine function describe. 
Then the specific test cases are defined by calling function it.
We're testing the sunny day scenario and border case that throws the exception.
At the beginning as a sanity check we can test if the function is defined.
Jasmine has a lot of advanced features, we can even use mocks, expectations and argument matchers.

Nothing stops us from writing tests first and going through red-green-refactor cycle.
Let's write a function that will count the number of specific elements on our page.

The behaviour of our component is defined by following spec:

describe('counter tests', function() {
it ('should be defined', function() {
expect(elementCounter).toBeDefined()
})
it ('should count 0 elements', function() {
expect(elementCounter.count()).toEqual(0)
})
it ('should count 1 element', function() {
var container = document.createElement('div')
container.setAttribute('id','myId')
document.body.appendChild(container)
expect(elementCounter.count()).toEqual(1)
})
})
view raw counter_spec.js hosted with ❤ by GitHub
Before testing the case with one element, we're adding it to the document. In more complex case we could load the HTML content.

To implement, it we'll use jquery (we need to add it to src/main/javascript):

var count = function () {
return $('#myId').length
}
elementCounter = {
count: count
}
view raw counter.js hosted with ❤ by GitHub
After executing mvn test we'll see a nice output:

-------------------------------------------------------
J A S M I N E S P E C S
-------------------------------------------------------
[INFO]
counter tests
should be defined
should count 0 elements
should count 1 element
incrementer tests
should be defined
should increment
should throw exception when argument is not a number
Results: 6 specs, 0 failures
view raw output hosted with ❤ by GitHub
Whole project can be found at github.