Test-driven Development (TDD) Using Javascript With QUnit
Test-driven development (TDD) :
A software development process that relies on the repetition of a very short
development cycle: first the developer writes a failing automated test
case that defines a desired improvement or new function, then produces
code to pass that test and finally refactors the new code to acceptable
standards.
ref: http://en.wikipedia.org/wiki/Test-
driven_development
Behavior-driven development (BDD)
Introducing BDD : http://blog.dannorth.net/introducing-
bdd/
"…where to start, what to test and what not to test, how much to test in one
go, what to call their tests, and how to understand why a test fails."
Basically use language/terminology that everyone on the project understands;
using a pattern (e.g. Given, When, Then.) to test expected behavior.
"Developers discovered it could do at least some of their documentation for
them, so they started to write test methods that were real sentences."
This article is about…
- Using QUnit with test-driven development (TDD)
- Example: utility method for weekend (only) content
- First, write tests to describe the expected behavior that fail
- Next, code to pass the tests
- The result: a short utility function, the test script describes what the code does (behavior), is unit tested, and doubles as documentation for the code.
TDD Process
- Add a test
- Run all tests and see if the new one fails
- Write some code
- Run the automated tests and see them succeed
- Refactor code
- Repeat
"Test-driven development constantly repeats the steps of adding test cases
that fail, passing them, and refactoring. Receiving the expected test results
at each stage reinforces the programmer's mental model of the code, boosts
confidence and increases productivity."
- Lather, Rinse, Repeat
Maybe the task is worth it…
From marketForce : For weekend traffic, the one word difference had a +17.58% RPV Lift (98.01% Confidence) and a +16.15% Conversion Lift (97.53% Confidence) So, I think it’s worth…
– this forecasts to an incremental $XXX,000 in annual revenues.
Time will only tell…
Could have used…
var today = new Date();
if (today.getDay() == 0 || today.getDay() == 6) {
$('#dr_billingContainer h3:eq(0)').html('XXXX XXXX');
}
…Instead chose to make a jQuery plugin that acts as a utility method that can
easily be reused for other sites
Javascript is all about behavior. Begin by writing some use cases or stories
of what users will experience.
Plugin / utility method : TODO…
/**
* $.fn.isWeekend() plugin to test if browsing on Sat./Sun.
* checks a date object to see if the day is a weekend day, Saturday / Sunday
* requires Date object as argument and jQuery
* dr.isWeekend alias for plugin to use as utility function
* @return true/false
*/
What's Needed? What behavior will we test for?
- is dr a global variable.
- dr.isWeekend() expects argument of object type Date
- dr.isWeekend() plugin returns true or false for each day of the week
QUnit : Start w/ HTML
<!DoCtYpE html>
<html>
<head>
<!-- QUnit CSS, JS, etc. -->
</head>
<body>
<h1 id="qunit-header">QUnit Tests for ...</h3>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>
<div id="qunit-fixture">test markup</div>
</body>
</html>
What does this look like? Let's see it in action with JSFIDDLE
Tip: click the 'Result' tab to see test results; then click the test to
expand (0, 1, 1) and see the details.
Setup testing : JSFIDDLE
Write a test : to fail
/* namespace */
module('namespace check');
test('is dr a global variable.',function(){
expect(1);
ok( window.dr, 'dr namespace is present');
});
Add namespace test : …fails
Add some code :
if (!window.dr) { var dr = {}; } // using dr as namespace
Code for namesapce : …passes
Add some helper code : in a module
module("dr.isWeekend() utility fn uses jQuery", {
setup: function() {
dr.date = new Date();
dr.weekdays = [1,2,3,4,5];
dr.weekends = [0,6];
},
teardown: function() {
delete dr.date;
delete dr.weekdays;
delete dr.weekends;
}
});
Add a module w/ fixture : to run with each test
Add a test : Arrange, Act, Assert
test("dr.isWeekend() expects argument of object type Date", function(){
// Arrange - use setup() for dr.date
var testPluginDefault;
// Act
testPluginDefault = dr.isWeekend();
// Assert
expect(1);
notStrictEqual( testPluginDefault, 'error', "Plugin does not return 'error' comparing with notStrictEqual");
});
Test for plugin / method : …fails
Code for plugin : skeleton
(function($) {
$.fn.isWeekend = function(options) {
var defaults = {};
opts = $.extend({},defaults, options);
// return this.each(function() {
// code plugin here ...
// });
};
dr.isWeekend = $.fn.isWeekend;
})(jQuery);
Code for plugin skeleton : …passes
Add more to the test : date object?
test("dr.isWeekend() expects argument of object type Date", function(){
// ...
failDate = [];
testPluginFalse = dr.isWeekend({ date: failDate });
// Assert
expect(2);
// ...
equal( testPluginFalse, 'invalid', "Plugin returns sting 'invalid' if argument is not Date object");
});
Add more to the test : …fails
Work it out :
$.fn.isWeekend = function(options) {
var defaults, opts;
defaults = { date: new Date() };
opts = $.extend({},defaults, options);
if (Object.prototype.toString.call(opts.date) === '[object Date]') {
opts.dateOk = true;
} else {
return 'invalid';
}
};
Code to : pass the test
More testing :
// Act
// ...
testPluginTrue = dr.isWeekend({ date: dr.date });
// Assert
expect(3);
// ...
notStrictEqual( testPluginTrue, 'invalid', "Plugin does not return 'invalid' comparing with notStrictEqual");
More testing : …passes, already :)
Write some tests for logic
test("dr.isWeekend() plugin returns true or false for each day of the week", function(){
// Arrange - use setup() for dr.date, dr.weekdays, dr.weekends
var n, weekday, weekend;
// Act
n = 0;
weekend = $.inArray(n, dr.weekends);
n = 1;
weekday = $.inArray(n, dr.weekdays);
// Assert
expect(2);
equal(weekend, 0, "testing a weekend value");
equal(weekday, 0, "testing a weekday value");
});
Write some tests for logic : …passes
Write some test for behavior :
// Assert
expect(11);
equal(weekend, 0, "testing a weekend value");
equal(weekday, 0, "testing a weekday value");
equal(isSunday, true, "Yes, 11/28/2010 is Sunday a weekend" );
equal(isMonday, false, "Yes, 11/29/2010 is Monday a weekday" );
equal(isTuesday, false, "Yes, Tuesday a weekday" );
equal(isWednesday, false, "Yes, Wednesday a weekday" );
equal(isThursday, false, "Yes, Thursday a weekday" );
equal(isFriday, false, "Yes, Friday a weekday" );
equal(isSaturday, true, "Yes, Saturday a weekday" );
equal(isTodayAWeekend, true, "Is today a weekend: true if today is a weekend" );
equal(isTodayAWeekend, false, "Is today a weekend: false if today is a weekday" );
Write some test for behavior : …fails
Code the expected behavior :
// ...
weekdays = [1,2,3,4,5];
weekends = [0,6];
if (Object.prototype.toString.call(opts.date) === '[object Date]') {
// check if weekend using getDay() -> returns number 0-6 for day of week
opts.n = opts.date.getDay();
if ( $.inArray(opts.n , weekends) > -1 ) {
return true;
} else if ( $.inArray(opts.n , weekdays) > -1 ) {
return false;
}
return 'error';
} else {
return 'invalid';
}
1 fail … everyday can't be a weekend :(
Another example : input helper text
Another example : input helper text
Links :
- This code - (github.com) gist | jsfiddle.net ( append 1/ - 11/ )
- This presentation Source - gist
- QUnit - Documentation | Code | JS | CSS
- Notes - TDD (wikipedia) | BDD (blog.dannorth.net) | 3-A's
- Tools - jslint.com | javascriptcompressor.com
- Presentation: Slides