Advanced QUnit - Front-End JavaScript Unit Testing

22
ADVANCED QUNIT FRONT-END JAVASCRIPT UNIT TESTING / Lars Thorup, ZeaLake @larsthorup

description

Code: https://github.com/larsthorup/qunit-demo-advanced 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 * Structuring tests for reuse and readability * Testing browser-specific behaviour * Leak testing

Transcript of Advanced QUnit - Front-End JavaScript Unit Testing

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

ADVANCED QUNITFRONT-END JAVASCRIPT UNIT TESTING

/ Lars Thorup, ZeaLake @larsthorup

Page 2: Advanced QUnit - 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 QUnit - 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 QUnit - Front-End JavaScript Unit Testing

QUNIT BASICSmodule('util.calculator', { setup: function () { this.calc = new Calculator(); }});

test('multiply', function () { equal(this.calc.multiply(6, 7), 42, '6*7');});

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

MOCKING, SPYING AND STUBBING

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

WHAT IS HARD TO TEST IN JAVASCRIPT?

Page 7: Advanced QUnit - 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 8: Advanced QUnit - Front-End JavaScript Unit Testing

MOCKING METHODSUsing SinonJS

We can mock the getMatch() method

decide how the mock should behave

verify that the mocked method was called correctlysinon.stub(autoComplete, 'getMatch').returns('monique');

$('#name').trigger($.Event('keypress', {charCode: 109}););

ok(autoComplete.getMatch.calledWith('m'));

equal($('#name').val(), 'monique');

Page 9: Advanced QUnit - 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;sinon.stub(window, 'open', function () { popup = { focus: sinon.spy() }; return popup;});

autoComplete.openPopup('zealake.com');

ok(window.open.calledWith('zealake.com', '_blank', 'resizable'));

ok(popup.focus.called, 'focus changed');

Page 10: Advanced QUnit - 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();

sinon.stub(window, 'AutoComplete', function () { this.focus = sinon.spy();});

ok(form, 'created');equal(window.AutoComplete.callCount, 1);var args = window.AutoComplete.firstCall.call.args;ok(args[0].is('#name'));deepEqual(args[1], {listUrl: '/someUrl'});

var autoComplete = window.AutoComplete.firstCall.call.thisValue;ok(autoComplete.focus.called);

Page 11: Advanced QUnit - 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 return

delayHide: function () { var self = this; setTimeout(function () { self.element.hide(); }, this.options.hideDelay);}

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

MOCKING TIMERSUse Sinon's mock clock

Tick the clock explicitly

Now the test completes in milliseconds

without waitingsinon.useFakeTimers();

autoComplete.delayHide();

ok($('#name').is(':visible'));

sinon.clock.tick(500);

ok($('#name').is(':hidden'));

Page 13: Advanced QUnit - 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 relies on that?

We cannot equal on a value that changes on every run

Instead, Sinon can mock the Date() constructor!

sinon.useFakeTimers();var then = new Date();

sinon.clock.tick(42000);var later = new Date();

equal(later.getTime() - then.getTime(), 42000);

Page 14: Advanced QUnit - 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']});});var autoComplete = new AutoComplete('#name', { listUrl: '/getNames'});sinon.clock.tick(can.fixture.delay);

respondWith(500); // Internal server error

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

DOM FIXTURESSupply the markup required by the code

Automatically cleanup markup after every test

Built into QUnit as #qunit-fixture

$('<input id="name">').appendTo('#qunit-fixture');

autoComplete = new AutoComplete('#name');

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

SPYING ON EVENTSHow do we test that an event was cancelled?

Spy on the preventDefault() method

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

var keypressEvent = $.Event('keypress', {charCode: 109});sinon.spy(keypressEvent, 'preventDefault');

$('#name').trigger(keypressEvent);

ok(keypressEvent.preventDefault.called);

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

SIMULATING CSS TRANSITIONS

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

PARAMERIZED AND CONDITIONED 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([ {desc: 'success', response: {list: ['x']}, expected: ['x']}, {desc: 'failure', response: 500, expected: []}], function (scenario) { test('listUrl option, ' + scenario.desc, function () { can.fixture('/getNames', function (original, respondWith) { respondWith(scenario.response); }); deepEqual(autoComplete.options.list, scenario.expected); });});

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

LEAK DETECTION

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

DOM ELEMENT LEAKSDOM Fixtures are cleaned up automatically

But sometimes code needs to go beyond the fixture,appending to $('body'), e.g for overlays

That code should have a way to clean up those elements

And our tests should invoke that cleanup

And we can easily verify that this is always done

teardown: function () { var leaks = $('body').children(':not(#qunit-reporter)'); equal(leaks.length, 0, 'no DOM elements leaked'); leaks.remove();}

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

MEMORY LEAKSwindow.performance.memory: a Google Chrome extension

run Chrome with --enable-memory-info --js-flags="--expose-gc"

Collect memory consumption data for every test

Sort and investigate the largest memory consumers

However, performance data is not reproducible

And the garbage collector disturbs the picture

But still usable

setup: function () { window.gc(); this.heapSizeBefore = window.performance.memory.usedJSHeapSize;},teardown: function () { window.gc(); this.heapSizeAfter = window.performance.memory.usedJSHeapSize; console.log(spec.heapSizeAfter - spec.heapSizeBefore);}

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

RESOURCESgithub.com/larsthorup/qunit-demo-advanced

@larsthorup

qunitjs.com

sinonjs.com

canjs.com

github.com/hakimel/reveal.js