This blog post is a continuation of the first part Introduction to Jasmine. Here we will analyze additional features of Jasmine test framework, specifically:
- Spy – functions/objects emulation
- Clock – callback synchronization using setTimeout/setInterval
- 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:
- HtmlReporter – tree-structure showing execution progress
- TrivialReporter – simple tree-structure (deprecated)
- 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