Advanced Jasmine - Front-End JavaScript Unit Testing

23
ADVANCED JASMINE FRONT-END JAVASCRIPT UNIT TESTING / Lars Thorup, ZeaLake @larsthorup

description

Code: https://github.com/larsthorup/jasmine-demo-advanced Video: https://www.youtube.com/watch?v=g4eQplHxU18 Audio: https://www.youtube.com/watch?v=8FUwc3gZDMw Unit testing front-end JavaScript presents its own unique set of challenges. In this session we will look at number of different techniques to tackle these challenges and make our JavaScript unit tests fast and robust. We plan to cover the following subjects: * Mocking and spy techniques to avoid dependencies on - Functions, methods and constructor functions - Time (new Date()) - Timers (setTimeout, setInterval) - Ajax requests - The DOM - Events * Expressive matchers - Jasmine-jQuery * Structuring tests for reuse and readability * Testing browser-specific behaviour

Transcript of Advanced Jasmine - Front-End JavaScript Unit Testing

Page 1: Advanced Jasmine - Front-End JavaScript Unit Testing

ADVANCED JASMINEFRONT-END JAVASCRIPT UNIT TESTING

/ Lars Thorup, ZeaLake @larsthorup

Page 2: Advanced Jasmine - Front-End JavaScript Unit Testing

WHO IS LARS THORUPSoftware 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

Page 3: Advanced Jasmine - Front-End JavaScript Unit Testing

AGENDAUnit 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

Page 4: Advanced Jasmine - Front-End JavaScript Unit Testing

JASMINE BASICSdescribe('Calculator', function () { var calc; beforeEach(function () { calc = new Calculator(); }); it('should multiply', function () { expect(calc.multiply(6, 7)).toBe(42); });});

Page 5: Advanced Jasmine - Front-End JavaScript Unit Testing

MOCKING, SPYING AND STUBBING

Page 6: Advanced Jasmine - Front-End JavaScript Unit Testing

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); }}

Page 7: Advanced Jasmine - Front-End JavaScript Unit Testing

MOCKING METHODSWe 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');

Page 8: Advanced Jasmine - Front-End JavaScript Unit Testing

MOCKING GLOBAL FUNCTIONSGlobal 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();

Page 9: Advanced Jasmine - Front-End JavaScript Unit Testing

MOCKING CONSTRUCTORSConstructors are functions

with this being 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();

Page 10: Advanced Jasmine - Front-End JavaScript Unit Testing

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 returndelayHide: function () { var self = this; setTimeout(function () { self.element.hide(); }, this.options.hideDelay);}

Page 11: Advanced Jasmine - Front-End JavaScript Unit Testing

MOCKING TIMERSUse Jasmine's mock clock

Control the clock explicitly

Now the test completes in milliseconds

without waitingjasmine.Clock.useMock();

autoComplete.delayHide();

expect($('#name')).toBeVisible();

jasmine.Clock.tick(500);

expect($('#name')).not.toBeVisible();

Page 12: Advanced Jasmine - Front-End JavaScript Unit Testing

MOCKING TIMEnew 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 expect on 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);

Page 13: Advanced Jasmine - Front-End JavaScript Unit Testing

MOCKING DATE() WITH JASMINEKeep 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;});

Page 14: Advanced Jasmine - Front-End JavaScript Unit Testing

MOCKING AJAX REQUESTSTo 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

Page 15: Advanced Jasmine - Front-End JavaScript Unit Testing

DOM FIXTURESSupply the markup required by the code

Automatically cleanup markup after every test

Various solutions

Built into QUnit as #qunit-fixture

Use jasmine-jqueryvar fixtures = jasmine.getFixtures();fixtures.set(fixtures.sandbox());

$('<input id="name">').appendTo('#sandbox');

autoComplete = new AutoComplete('#name');

Page 16: Advanced Jasmine - Front-End JavaScript Unit Testing

SPYING ON EVENTSHow 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();

Page 17: Advanced Jasmine - Front-End JavaScript Unit Testing

SIMULATING CSS TRANSITIONS

Page 18: Advanced Jasmine - Front-End JavaScript Unit Testing

JASMINE MATCHERS

Page 19: Advanced Jasmine - Front-End JavaScript Unit Testing

EXPRESSIVE MATCHERSMake your tests more readable

Use jasmine-jquery for jQuery-specific matchers

Instead of:

Prefer:

expect($('#name').is(':visible')).toBeFalsy();

expect($('#name')).not.toBeVisible();

Page 20: Advanced Jasmine - Front-End JavaScript Unit Testing

ROLL YOUR OWN MATCHERSMake 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; }});

Page 21: Advanced Jasmine - Front-End JavaScript Unit Testing

STRUCTURE OF TEST CODEReuse 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(); }); });});

Page 22: Advanced Jasmine - Front-End JavaScript Unit Testing

BROWSER-SPECIFIC TESTSSome 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 () { }); });});

Page 23: Advanced Jasmine - Front-End JavaScript Unit Testing

RESOURCESgithub.com/larsthorup/jasmine-demo-advanced

@larsthorup

pivotal.github.io/jasmine

github.com/velesin/jasmine-jquery

canjs.com

github.com/hakimel/reveal.js