On Github hoontw / principles-of-unit-testing
…insanely synchronized North Korean children
Principle 1
Make clear what your unit is
aka scope/definitionCase Studies
What would you test? What is a unit to you? Do you have to look at the code to define a unit?Principle 2
Figure out how to access and observe the unit
-giant entry point, with many private functionsvar RubberDuck = function (name) { this._name = name; this.squawk = function () {...}; this.swim = function () {...}; }; /* constructor */ var instance = new RubberDuck('Ducky');
var Utilities = { encodeUri = function (uri) {...}; roundUp = function (number) {...}; quote = function (string) {...}; }; /* global */ Utilities;
/* vivification */ jQuery('#my-awesome-list').carousel(); /* another example */ var myList = document.querySelector('#my-awesome-list'); Carousel.create(myList);Bootstrap carousel
Create test environment
Get an instance of
Hints
Now that I have the unit, what can I do with it?
function add(number1, number2) { if (number1 === 90210) { return -1; // That is my lucky number! :) LOL FTW } else { return number1 + number2; } }
What can you do with instance? What can you observe?
var CountrySelector = function () { var default = 'Canada', submitButton = document.querySelector('#submit-country'); var abbreviate = function (name) { return name.substring(0, 3); }; this.countryLength = 2; this.getCountryCode = function (name) { return (name === 'Canada') ? 'CA' : null; }; submitButton.addEventListener('click', function () { // make AJAX call to server submitButton.style.top = "0px"; // move box to top submitButton.style.color = "green"; // and turn it green }); }; var instance = new CountrySelector();
var CountrySelector = function () { var default = 'Canada', submitButton = document.querySelector('#submit-country'); var abbreviate = function (name) { return name.substring(0, 3); }; /*** Public properties and functions ***/ this.countryLength = 2; this.getCountryCode = function (name) { return (name === 'Canada') ? 'CA' : null; }; /*** Event handlers ***/ submitButton.addEventListener('click', function () { /*** Effects ***/ // make AJAX call to server submitButton.style.top = "0px"; // move box to top submitButton.style.color = "green"; // and turn it green }); }; var instance = new CountrySelector(); instance.countryLength; // 2 instance.getCountryCode('Canada'); // 'CA' // clicking submit button makes a server call and moves the button
The Black Box
Pre-Principle
Build a component with testing in mind, not an afterthought
aka Consideration - first-class concern - security, maintainability, localization, accessibilityPrinciple 3
Eliminate or control external influences
isolate dependenciesThings that…
Identify the things that should be controlled for
Modernizr CSS prefixes
github.com/Modernizr/Modernizr/blob/master/src/cssomPrefixes.jsTypeahead Dropdown
github.com/twitter/typeahead.js/blob/master/src/typeahead/dropdown.js we can replace a dependency by a mock or stub during testing if that dependency is injected into the object under test. We cannot easily replace those that the tested object instantiates or retrieves explicitly by itself.// Assume unit calls Dropdown.open() // Override Dropdown.open() and do nothing when called sinon.stub(Dropdown, 'open'); // Return false when called sinon.stub(Dropdown, 'open').returns(false); // Throw an exception when called sinon.stub(Dropdown, 'open').throws();
Principle 4
Restrict interference between tests
What's wrong here?
var hippo = new Hippo(); test('initalize makes hippo ready for action', function () { hippo.initalize(); strictEqual(hippo.isReadyForAction(), true); }); test('rest relaxes hippo', function () { hippo.rest(); strictEqual(hippo.isRelaxed(), true); }); test('yawn makes hippo sluggish', function () { hippo.yawn(); strictEqual(hippo.isSluggish(), true); });Not idempotent, sensitive to certain order
What's wrong here? (2)
var favouriteFoods = ['kale', 'poutine']; test('eatOne removes a food item', function () { var hippo = new Hippo(); hippo.eatOne(favouriteFoods); strictEqual(favouriteFoods.length, 1); }); test('eatAll removes all food items', function () { var hippo = new Hippo(); hippo.eatAll(favouriteFoods); strictEqual(favouriteFoods.length, 0); });Not idempotent, sensitive to certain order, changes to shared data
What's wrong here? (3)
test('draw appends a on the specified element', function () { var hippo = new Hippo(); hippo.draw(document.body); // look for img tag on document.body var imgElement = document.body.querySelectorAll('img'); strictEqual(imgElement.length, 1); });Not idempotent, changes to global environment
What's wrong here? (4)
test('dance creates shiny confetti and makes #fruity purple', function () { var hippo = new Hippo(), testElement = document.createElement('div'); // arrange testElement.id = 'fruity'; document.body.appendChild(testElement); // act var result = hippo.dance(); var pageColour = window.getComputedStyle(testElement) .getPropertyValue('color'); document.body.removeChild(testElement); // assert strictEqual(result, 'confetti'); strictEqual(pageColour, 'purple'); });Testing more than one thing at a time
Tests should…
Principle 5
Create practical test cases
module('hippoTestSuite', { setup: function () { // common setup actions... }, teardown: function () { // common teardown actions... } }); test('Description of test', function () { // arrange // act // assert strictEqual(expected, actualResult, 'optional error message'); }); test('Description of another test', function () { // arrange, act, assert strictEqual(expected, actualResult, 'optional error message'); });
Write test cases for at least one of these
QUnit template - jsbin.com/xomiwi/1/edit
var humanizeDates = function (date1, date2) { if (date1 < date2) { if (date2 - date1 > 500) { return 'very long time ago'; } return 'long time ago'; } else if (date1 > date2) { if (date1 - date2 > 500) { return 'very far in future'; } return 'in future'; } return 'same time'; };
Statement coverage = % statements executed in tests
Equivalent partition = range of values that should give the same result
Boundary value = value that separates adjacent equivalent partitions
moment(someDate).isAfter(anotherDate) eq partition 1 eq partition 2 |--------------------------|---------------------------| 0 now max Date
test('bump causes hippo to trigger a burp event', function () { var burpSpy = sinon.spy(); // arrange var hippo = new Hippo(); $('body').on('burp', burpSpy); // act hippo.bump(); // assert strictEqual(burpSpy.calledOnce, true); });
Principle 6
Create understandable tests
test('computeCommonFactor returns -1', function () { var l = new Factor(), div = document.createElement('div'); sinon.stub(someMathLib, 'factorize').returns(2); div.id = 'factor'; l.init(div); strictEqual(l.computeCommonFactor(-123), -1); l.destroy(); someMathLib.factorize.restore(); });
test('computeCommonFactor returns -1 when an invalid argument is given', function () { var factor = new Factor(), fixture = document.createElement('div'), invalidArgument = -123, result; // arrange sinon.stub(someMathLib, 'factorize').returns(2); fixture.id = 'factor'; factor.init(fixture); // act result = factor.computeCommonFactor(invalidArgument); factor.destroy(); someMathLib.factorize.restore(); // assert strictEqual(result, -1); });
test('hide fires a hidden event', function () { var accordionElement = document.createElement('ul'), accordionTab1 = document.createElement('li'), accordionTab2 = document.createElement('li'), listener = sinon.spy(); // arrange sinon.stub(someLib, 'accordify', function (options) { if (options.foo === true) { return { goo: function () { return true; }, hoo: function () { return ''; } }; } return { goo: function () {}, hoo: function () {} }; }); accordionElement.classList.add('myAccordion'); accordionTab1.classList.add('tab'); accordionTab2.classList.add('tab'); accordionElement.appendChild(accordionTab1); accordionElement.appendChild(accordionTab2); qunitFixture.appendChild(accordionElement); accordion = jQuery('.myAccordion').accordion(); // init jQuery.on('hidden', listener); // act accordion.hide(); // assert ok(listener.calledOnce); // cleanup jQuery.off('hidden', listener); accordion.destroy(); someLib.accordify.restore(); });
var standardAccordion; module('accordion', { setup: function () { setupAccordifyStub(); standardAccordion = createAccordion(); }, teardown: function () { standardAccordion.destroy(); someLib.accordify.restore(); } }); test('hide fires a hidden event', function () { var hiddenEventListener = sinon.spy(); // arrange jQuery.on('hidden', hiddenEventListener); // act standardAccordion.hide(); // assert ok(hiddenEventListener.calledOnce); // cleanup jQuery.off('hidden', hiddenEventListener); });
Critique unit test for jQuery UI accordion - https://github.com/jquery/jquery-ui/blob/master/tests/unit/accordion/accordion_events.js
Principle 7
Reduce barriers to working with tests
Insanely Synchronous Children In An Impressive Ceremony
Images
This work is licensed under a Creative Commons Attribution 4.0 International License.
Suggestions or errata? Contact TW Hoon at GitHub (username: hoontw).