Jasmine HTML Reporter
This blog post is a continuation of the first part Introduction to Jasmine. Here we will analyze additional features of Jasmine test framework, specifically:

  1. Spy – functions/objects emulation
  2. Clock – callback synchronization using setTimeout/setInterval
  3. Runner and Reporter – running the specs and presenting results to the user

For convenience, we will examine testing in a browser environment, while the code examples will be provided in CoffeeScript (examples in plain JavaScript).

For tracking function calls and call arguments in Jasmine you should use spyOn. The function spyOn takes two parameters – the target object for which the function is called and the function name:

spyOn(window, 'isNaN')

The standard use of spyOn does not call the original function.

Let's take a look at the below example describing the small class Person:

class Person
  name: null
  age:  0
  constructor: (@name, @age) ->
  getName: -> @name
  setName: (value) -> @name = value
  getAge: -> @age
  addYear: -> @age += 1

Testing specs using spyOn allows to track the number of calls, all the arguments of its calls and each call separately:

describe "Spy", ->
  person = null

  beforeEach ->
    person = new Person("Jim", 25)

  it "was called", ->
    spyOn(person, 'getName')
    person.getName()
    expect(person.getName).toHaveBeenCalled()

  it "tracks the number of calls", ->
    spyOn(person, 'addYear')
    person.addYear()
    person.addYear()
    expect(person.addYear.calls.length).toEqual(2)

  it "tracks call arguments", ->
    spyOn(person, 'setName')
    person.setName("Ira")
    expect(person.setName).toHaveBeenCalledWith("Ira") # can have multiple arguments

  it "has access to the last call", ->
    spyOn(person, 'setName')
    person.setName("Ira")
    expect(person.setName.mostRecentCall.args[0]).toEqual("Ira")

  it "has access to all calls", ->
    spyOn(person, 'setName')
    person.setName("Ira")
    expect(person.setName.calls[0].args[0]).toEqual("Ira")

When using spyOn together with andCallThrough, the original function will be called:

  it "calls original function", ->
    spyOn(person, 'getName').andCallThrough()
    expect(person.getName()).toEqual("Jim")
    expect(person.getName).toHaveBeenCalled()

When using spyOn together with andReturn, all calls to the function will return the supplied value:

  it "returns fake value", ->
    spyOn(person, 'getName').andReturn("Dan")
    expect(person.getName()).toEqual("Dan")
    expect(person.getName).toHaveBeenCalled()

When combining spyOn with andCallFake, instead of calling the original function the spy will delegate to the supplied function:

  it "calls fake function", ->
    spyOn(person, 'getAge').andCallFake(-> return 5 * 11)
    expect(person.getAge()).toEqual(55)
    expect(person.getAge).toHaveBeenCalled()

When it is required to create a function without any implementation behind it, you may create a "bare" spy using createSpy. You can still use all the matchers available for a regular spy while interacting with the "bare" spy. In order to create a mock object with a set of functions you can use createSpyObj:

  it "creates fake function", ->
    concat = jasmine.createSpy('CONCAT')
    concat("one", "two")
    expect(concat.identity).toEqual('CONCAT') # has name to identify
    expect(concat).toHaveBeenCalledWith("one", "two")
    expect(concat.calls.length).toEqual(1)

  it "creates fake object", ->
    button = jasmine.createSpyObj('BUTTON', ['click', 'setTitle', 'getTitle'])
    button.click()
    button.setTitle("Help")
    expect(button.click).toBeDefined()
    expect(button.click).toHaveBeenCalled()
    expect(button.setTitle).toHaveBeenCalledWith("Help")
    expect(button.getTitle).not.toHaveBeenCalled()

Synchronous testing of setTimeout/setInterval calls is performed using jasmine.Clock.useMock, whereas ticking the clock forward can be done by calling jasmine.Clock.tick, which is assigned a number in milliseconds:

describe "Clock", ->
  callback = null

  beforeEach ->
    callback = jasmine.createSpy('TIMER')
    jasmine.Clock.useMock()

  it "calls timeout function synchronously", ->
    setTimeout((-> callback()), 100) # set timeout 100ms
    expect(callback).not.toHaveBeenCalled()
    jasmine.Clock.tick(101) # move clock forward 101ms
    expect(callback).toHaveBeenCalled()

If you need to check the object type, you can use jasmine.any function with expected type as argument value:

  it "checks type name", ->
    spyOn(person, 'setName')
    person.setName("Ira")
    expect(person.setName).toHaveBeenCalledWith(jasmine.any(String))

Usually for running tests in Jasmine you should write a small script (Runner):

# Runner
jasmineEnv = jasmine.getEnv()
jasmineEnv.updateInterval = 250

currentWindowOnload = window.onload

window.onload = ->
  currentWindowOnload() if currentWindowOnload
  execJasmine()

execJasmine = -> jasmineEnv.execute()

# Reporter
htmlReporter = new jasmine.HtmlReporter()
jasmineEnv.addReporter(htmlReporter)
jasmineEnv.specFilter = (spec) -> htmlReporter.specFilter(spec)

Presenting results to the users can be handled in several ways. Jasmine supports different types of test reports (Reporters), the main ones being:

  1. HtmlReporter – tree-structure showing execution progress
  2. TrivialReporter – simple tree-structure (deprecated)
  3. ConsoleReporter – displays test results in the console (for node.js)

If you have additional questions or comments, we will be glad to hear them.

Reference:

github.com/pivotal/jasmine – project page on GitHub
github.com/inex-finance/blog-examples/tree/master/jasmine – code examples used in this article
inex-finance.github.com/blog-examples/jasmine/advanced.html – Jasmine test report for this article


Published at February 17, 2013 09:55
Updated at February 17, 2013 10:23

comments powered by Disqus