…insanely synchronized North Korean children
Principle 1
Make clear what your unit is
Case Studies
Principle 2
Figure out how to access and observe the unit
var 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
Principle 3
Eliminate or control external influences
Things 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// 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);
});
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);
});
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);
});
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');
});
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).