ADVANCED JASMINE
FRONT-END JAVASCRIPT UNIT TESTING
/Lars Thorup, ZeaLake @larsthorup
WHO IS LARS THORUP
Software developer/architect
C++, C# and JavaScript
Test Driven Development
Coach: Teaching agile and automated testing
Advisor: Assesses software projects and companies
Founder and CEO of ZeaLake
AGENDA
Unit tests gives quality feedback
Make them fast
Make them precise
Run thousands of unit tests in seconds
We will look at
Mocking techniques
Front-end specific testing patterns
Assuming knowledge about JavaScript and unit testing
JASMINE BASICS
describe('Calculator', function () {
var calc;
beforeEach(function () {
calc = new Calculator();
});
it('should multiply', function () {
expect(calc.multiply(6, 7)).toBe(42);
});
});
MOCKING, SPYING AND STUBBING
HOW TO TEST IN ISOLATION?
We want to test code in isolation
here the code is the 'keypress' event handler
and isolation means not invoking the getMatch() method
'keypress': function (element, event) {
var pattern = this.element.val();
pattern += String.fromCharCode(event.charCode);
var match = this.getMatch(pattern);
if (match) {
event.preventDefault();
this.element.val(match);
}
}
MOCKING METHODS
We can mock the getMatch() method
decide how the mock should behave
verify that the mocked method was called correctly
spyOn(autoComplete, 'getMatch').andReturn('monique');
$('#name').trigger($.Event('keypress', {charCode: 109}));
expect(autoComplete.getMatch).toHaveBeenCalledWith('m');
expect($('#name')).toHaveValue('monique');
MOCKING GLOBAL FUNCTIONS
Global functions are properties of the window object
openPopup: function (url) {
var popup = window.open(url, '_blank', 'resizable');
popup.focus();
}
var popup;
spyOn(window, 'open').andCallFake(function () {
popup = {
focus: jasmine.createSpy()
};
return popup;
});
autoComplete.openPopup('zealake.com');
expect(window.open).toHaveBeenCalledWith('zealake.com', '_blank', 'resizable');
expect(popup.focus).toHaveBeenCalledWith();
MOCKING CONSTRUCTORS
Constructors are functions
with thisbeing the object to construct
this.input = new window.AutoComplete(inputElement, {
listUrl: this.options.listUrl
});
this.input.focus();
spyOn(window, 'AutoComplete').andCallFake(function () {
this.focus = jasmine.createSpy();
});
expect(window.AutoComplete.callCount).toBe(1);
var args = window.AutoComplete.mostRecentCall.args;
expect(args[0]).toBe('#name');
expect(args[1]).toEqual({listUrl: '/someUrl'});
var object = window.AutoComplete.mostRecentCall.object;
expect(object.focus).toHaveBeenCalledWith();
HOW TO AVOID WAITING?
We want the tests to be fast
So don't use Jasmine waitsFor()
But we often need to wait
For animations to complete
For AJAX responses to return
delayHide: function () {
var self = this;
setTimeout(function () {
self.element.hide();
}, this.options.hideDelay);
}
MOCKING TIMERS
Use Jasmine's mock clock
Control the clock explicitly
Now the test completes in milliseconds
without waiting
jasmine.Clock.useMock();
autoComplete.delayHide();
expect($('#name')).toBeVisible();
jasmine.Clock.tick(500);
expect($('#name')).not.toBeVisible();
MOCKING TIME
new Date()tends to return different values over time
Actually, that's the whole point :)
But how do we test code that does that?
We cannot expecton a value that changes on every run
We can mock the Date()constructor!
var then = new Date();
jasmine.Clock.tick(42000);
var now = new Date();
expect(now.getTime() - then.getTime()).toBe(42000);
MOCKING DATE() WITH JASMINE
Keep Date() and setTimeout() in sync
jasmine.GlobalDate = window.Date;
var MockDate = function () {
var now = jasmine.Clock.defaultFakeTimer.nowMillis;
return new jasmine.GlobalDate(now);
};
MockDate.prototype = jasmine.GlobalDate.prototype;
window.Date = MockDate;
jasmine.getEnv().currentSpec.after(function () {
window.Date = jasmine.GlobalDate;
});
MOCKING AJAX REQUESTS
To test in isolation
To vastly speed up the tests
Many options
can.fixture
Mockjax
Sinon
can.fixture('/getNames', function (original, respondWith) {
respondWith({list: ['rachel', 'lakshmi']});
});
autoComplete = new AutoComplete('#name', {
listUrl: '/getNames'
});
jasmine.Clock.tick(can.fixture.delay);
respondWith(500); // Internal server error
DOM FIXTURES
Supply the markup required by the code
Automatically cleanup markup after every test
Various solutions
Built into QUnit as #qunit-fixture
Use jasmine-jquery
var fixtures = jasmine.getFixtures();
fixtures.set(fixtures.sandbox());
$('<input id="name">').appendTo('#sandbox');
autoComplete = new AutoComplete('#name');
SPYING ON EVENTS
How do we test that an event was triggered?
Or prevented from bubbling?
Use jasmine-jquery!
'keypress': function (element, event) {
var pattern = this.element.val() +
String.fromCharCode(event.charCode);
var match = this.getMatch(pattern);
if(match) {
event.preventDefault();
this.element.val(match);
}
}
keypressEvent = spyOnEvent('#name', 'keypress');
$('#name').trigger($.Event('keypress', {charCode: 109}));
expect(keypressEvent).toHaveBeenPrevented();
SIMULATING CSS TRANSITIONS
JASMINE MATCHERS
EXPRESSIVE MATCHERS
Make your tests more readable
Use jasmine-jquery for jQuery-specific matchers
Instead of:
Prefer:
expect($('#name').is(':visible')).toBeFalsy();
expect($('#name')).not.toBeVisible();
ROLL YOUR OWN MATCHERS
Make your tests even more readable
Like this can.js specific matcher:
Defined like this:
github.com/pivotal/jasmine/wiki/Matchers
expect($('#name')).toHaveControlOfType(AutoComplete);
jasmine.getEnv().currentSpec.addMatchers({
toHaveControlOfType: function (expected) {
var actual = this.actual.controls(expected);
return actual.length > 0;
}
});
STRUCTURE OF TEST CODE
Reuse common setup code
By nesting Jasmine's describe()functions
describe('delayHide', function () {
beforeEach(function () {
autoComplete.delayHide();
});
it('should initially stay visible', function () {
expect($('#name')).toBeVisible();
});
describe('after a delay', function () {
beforeEach(function () {
jasmine.Clock.tick(500);
});
it('should be invisible', function () {
expect($('#name')).not.toBeVisible();
});
});
});
BROWSER-SPECIFIC TESTS
Some code is browser specific
maybe using a browser specific API
and might only be testable in that browser
Tests can be conditioned
Or iterated...
can.each([
{
response: {list: ['rachel', 'lakshmi']},
expected: ['rachel', 'lakshmi']
},
{
response: 500,
expected: []
}
], function (scenario) {
describe('when ' + JSON.stringify(scenario.response), function () {
it('should ' + JSON.stringify(scenario.expected), function () {
});
});
});
RESOURCES
github.com/larsthorup/jasmine-demo-advanced
@larsthorup
pivotal.github.io/jasmine
github.com/velesin/jasmine-jquery
canjs.com
github.com/hakimel/reveal.js

Advanced Jasmine - Front-End JavaScript Unit Testing

  • 1.
    ADVANCED JASMINE FRONT-END JAVASCRIPTUNIT TESTING /Lars Thorup, ZeaLake @larsthorup
  • 2.
    WHO IS LARSTHORUP Software developer/architect C++, C# and JavaScript Test Driven Development Coach: Teaching agile and automated testing Advisor: Assesses software projects and companies Founder and CEO of ZeaLake
  • 3.
    AGENDA Unit tests givesquality feedback Make them fast Make them precise Run thousands of unit tests in seconds We will look at Mocking techniques Front-end specific testing patterns Assuming knowledge about JavaScript and unit testing
  • 4.
    JASMINE BASICS describe('Calculator', function() { var calc; beforeEach(function () { calc = new Calculator(); }); it('should multiply', function () { expect(calc.multiply(6, 7)).toBe(42); }); });
  • 5.
  • 6.
    HOW TO TESTIN ISOLATION? We want to test code in isolation here the code is the 'keypress' event handler and isolation means not invoking the getMatch() method 'keypress': function (element, event) { var pattern = this.element.val(); pattern += String.fromCharCode(event.charCode); var match = this.getMatch(pattern); if (match) { event.preventDefault(); this.element.val(match); } }
  • 7.
    MOCKING METHODS We canmock the getMatch() method decide how the mock should behave verify that the mocked method was called correctly spyOn(autoComplete, 'getMatch').andReturn('monique'); $('#name').trigger($.Event('keypress', {charCode: 109})); expect(autoComplete.getMatch).toHaveBeenCalledWith('m'); expect($('#name')).toHaveValue('monique');
  • 8.
    MOCKING GLOBAL FUNCTIONS Globalfunctions are properties of the window object openPopup: function (url) { var popup = window.open(url, '_blank', 'resizable'); popup.focus(); } var popup; spyOn(window, 'open').andCallFake(function () { popup = { focus: jasmine.createSpy() }; return popup; }); autoComplete.openPopup('zealake.com'); expect(window.open).toHaveBeenCalledWith('zealake.com', '_blank', 'resizable'); expect(popup.focus).toHaveBeenCalledWith();
  • 9.
    MOCKING CONSTRUCTORS Constructors arefunctions with thisbeing the object to construct this.input = new window.AutoComplete(inputElement, { listUrl: this.options.listUrl }); this.input.focus(); spyOn(window, 'AutoComplete').andCallFake(function () { this.focus = jasmine.createSpy(); }); expect(window.AutoComplete.callCount).toBe(1); var args = window.AutoComplete.mostRecentCall.args; expect(args[0]).toBe('#name'); expect(args[1]).toEqual({listUrl: '/someUrl'}); var object = window.AutoComplete.mostRecentCall.object; expect(object.focus).toHaveBeenCalledWith();
  • 10.
    HOW TO AVOIDWAITING? We want the tests to be fast So don't use Jasmine waitsFor() But we often need to wait For animations to complete For AJAX responses to return delayHide: function () { var self = this; setTimeout(function () { self.element.hide(); }, this.options.hideDelay); }
  • 11.
    MOCKING TIMERS Use Jasmine'smock clock Control the clock explicitly Now the test completes in milliseconds without waiting jasmine.Clock.useMock(); autoComplete.delayHide(); expect($('#name')).toBeVisible(); jasmine.Clock.tick(500); expect($('#name')).not.toBeVisible();
  • 12.
    MOCKING TIME new Date()tendsto return different values over time Actually, that's the whole point :) But how do we test code that does that? We cannot expecton a value that changes on every run We can mock the Date()constructor! var then = new Date(); jasmine.Clock.tick(42000); var now = new Date(); expect(now.getTime() - then.getTime()).toBe(42000);
  • 13.
    MOCKING DATE() WITHJASMINE Keep Date() and setTimeout() in sync jasmine.GlobalDate = window.Date; var MockDate = function () { var now = jasmine.Clock.defaultFakeTimer.nowMillis; return new jasmine.GlobalDate(now); }; MockDate.prototype = jasmine.GlobalDate.prototype; window.Date = MockDate; jasmine.getEnv().currentSpec.after(function () { window.Date = jasmine.GlobalDate; });
  • 14.
    MOCKING AJAX REQUESTS Totest in isolation To vastly speed up the tests Many options can.fixture Mockjax Sinon can.fixture('/getNames', function (original, respondWith) { respondWith({list: ['rachel', 'lakshmi']}); }); autoComplete = new AutoComplete('#name', { listUrl: '/getNames' }); jasmine.Clock.tick(can.fixture.delay); respondWith(500); // Internal server error
  • 15.
    DOM FIXTURES Supply themarkup required by the code Automatically cleanup markup after every test Various solutions Built into QUnit as #qunit-fixture Use jasmine-jquery var fixtures = jasmine.getFixtures(); fixtures.set(fixtures.sandbox()); $('<input id="name">').appendTo('#sandbox'); autoComplete = new AutoComplete('#name');
  • 16.
    SPYING ON EVENTS Howdo we test that an event was triggered? Or prevented from bubbling? Use jasmine-jquery! 'keypress': function (element, event) { var pattern = this.element.val() + String.fromCharCode(event.charCode); var match = this.getMatch(pattern); if(match) { event.preventDefault(); this.element.val(match); } } keypressEvent = spyOnEvent('#name', 'keypress'); $('#name').trigger($.Event('keypress', {charCode: 109})); expect(keypressEvent).toHaveBeenPrevented();
  • 17.
  • 18.
  • 19.
    EXPRESSIVE MATCHERS Make yourtests more readable Use jasmine-jquery for jQuery-specific matchers Instead of: Prefer: expect($('#name').is(':visible')).toBeFalsy(); expect($('#name')).not.toBeVisible();
  • 20.
    ROLL YOUR OWNMATCHERS Make your tests even more readable Like this can.js specific matcher: Defined like this: github.com/pivotal/jasmine/wiki/Matchers expect($('#name')).toHaveControlOfType(AutoComplete); jasmine.getEnv().currentSpec.addMatchers({ toHaveControlOfType: function (expected) { var actual = this.actual.controls(expected); return actual.length > 0; } });
  • 21.
    STRUCTURE OF TESTCODE Reuse common setup code By nesting Jasmine's describe()functions describe('delayHide', function () { beforeEach(function () { autoComplete.delayHide(); }); it('should initially stay visible', function () { expect($('#name')).toBeVisible(); }); describe('after a delay', function () { beforeEach(function () { jasmine.Clock.tick(500); }); it('should be invisible', function () { expect($('#name')).not.toBeVisible(); }); }); });
  • 22.
    BROWSER-SPECIFIC TESTS Some codeis browser specific maybe using a browser specific API and might only be testable in that browser Tests can be conditioned Or iterated... can.each([ { response: {list: ['rachel', 'lakshmi']}, expected: ['rachel', 'lakshmi'] }, { response: 500, expected: [] } ], function (scenario) { describe('when ' + JSON.stringify(scenario.response), function () { it('should ' + JSON.stringify(scenario.expected), function () { }); }); });
  • 23.