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
Top Related