Advanced QUnit - Front-End JavaScript Unit Testing

Preview:

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

Citation preview

ADVANCED QUNITFRONT-END JAVASCRIPT UNIT TESTING

/ Lars Thorup, ZeaLake @larsthorup

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

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

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

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

MOCKING, SPYING AND STUBBING

WHAT IS HARD TO TEST IN JAVASCRIPT?

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

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

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

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

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

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

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

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

SIMULATING CSS TRANSITIONS

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

LEAK DETECTION

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

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

RESOURCESgithub.com/larsthorup/qunit-demo-advanced

@larsthorup

qunitjs.com

sinonjs.com

canjs.com

github.com/hakimel/reveal.js