TDD with QUnit
Test-driven development using javascript


by Bill Heaton

Who writes tests anyway?

QUnit is a powerful, easy-to-use, JavaScript test suite.

It's used by the jQuery project to test its code and plugins but is capable of testing any generic JavaScript code

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 talk is about...

  1. Using Qunit with Test-driven development (TDD)
  2. Example utility method for weekend (only) content change
  3. First write tests which describe the expected behavior which fail
  4. Then code the methods to pass the tests
  5. The result is a short utility function that which describes what it does and is fully tested showing it's behavior

TDD Process

  1. Add a test
  2. Run all tests and see if the new one fails
  3. Write some code
  4. Run the automated tests and see them succeed
  5. Refactor code
  6. 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."

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 $300K 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('Billing Information');
              }
            

…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 ...</h1>
                <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

Setup testing : http://jsfiddle.net/9xkh7/

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

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