"Doing" Development Journal - Tests and Formatting

Previously I implemented a basic JavaScript model for activities. I skipped formatting the durations, so instead of showing that an activity took 3 hours, it displays milliseconds, not terribly user friendly. I will sketch out how I would like those durations to be formatted, define some tests, and implement the formatting code.

There are two common formats that could be used, rounded units such as "3h", or a timer format like "3:00:00". The prior is easier to understand, but less precise. The latter is easier to compare when the units differ, ie: "3:00:00" vs. "0:15:00". Although I used the "3h" format in the mockup, I think I'll try using the timer format first since I could also use it for the timer. I will define the format as being "hh:mm:ss". Here are some example durations:

    00:00 -- The minimum possible value
    00:12 -- A very small value
129:59:59 -- An extremely large value
 11:32:15 -- Two digit hour
  1:23:45 -- One digit hour

Notice that in the examples, we only zero pad the minutes and seconds, while the hours are only displayed if present. With a decent description of the format, I can start writing tests, then implement the formatting function.

There are as many JavaScript testing libraries as there are developers, and most of them work just fine. Since I generally trust TJ Holowaychuk, I'll use his library Mocha, and the Chai library for assertions. We'll add a test directory and the required files for using Mocha and Chai:

test/
  mocha.js    - Mocha test library
  chai.js     - Chai assertion library
  mocha.css   - Styling for mocha's output
  tests.js    - Our javascript tests
  tests.html  - Html file that loads mocha, chai, and our tests

Everything except for the tests.js can just be copied from Mocha and Chai. I will write a nice failing test to make sure this is working:

describe('Doing', function(){
  describe('formatting durations', function(){
    it('works', function(){
      assert(false);
    });
  });
});

Launching the tests.html shows a failure, success! The test output is absolutely correct right now, Doing fails at formatting durations. Now it's my job to do something about that. First I will write a very general test, the function should return a String:

it('returns a String', function(){
  var duration = 35 * 1000; // 35 seconds
  var string   = format(duration);
  assert.typeOf(string, 'string');
});

Running this obviously fails: "ReferenceError: format is not defined". First we will define a method in doing.js, then we will make it available in the tests. I will create a Format object to stash all of the formatting functions on, this helps keep things a little tidier:

Format = {};
Format.duration = function(millis){
  return "";
};

The tests will call this function often so I will assign Format.duration to format in the context of the duration formatting tests:

describe('formatting durations', function(){
    var format = Format.duration;
  ...
});

Now the test passes, I am on the right track. Of course this isn't very interesting, so let's use each of the examples above as tests. I will be testing a lot of durations, so I will introduce a helper function to make them for me:

// Helper for testing, returns a duration in milliseconds:
function makeDuration(hours, minutes, seconds){
  return (hours * 3600  + minutes * 60 + seconds) * 1000;
}

Now the test for formatting a duration is simpler:

it('formats an empty duration', function(){
    var duration = makeDuration(0,0,0);
    var string   = format(duration);

  assert.equal(string, '00:00');
});

Not surprisingly this fails with, expected '' to equal '00:00'. Great! Now I have something to shoot for. I'll repeat this pattern for each of the example durations outlined above, and then get down to writing the code the make them all pass. We simply need to break the duration into hours, minutes, and seconds, then pad the minutes and seconds out to two digits, and put them back together with colons separating each part. I am positive there is a cleaner solution, but let's get something working first:

Format.duration = function(millis){
  var hours, minutes, seconds;

  hours   = Math.floor(millis / 3600000);
  millis  = millis % 3600000;

  minutes = Math.floor(millis / 60000);
  millis  = millis % 60000;

  seconds = Math.floor(millis / 1000);

  var string = "";
  if (hours > 0){
    string += hours + ":";
  }
  string += (minutes < 10 ? "0"+minutes : minutes) + ":";
  string += (seconds < 10 ? "0"+seconds : ""+seconds);

  return string;
};

Running this passes all the tests, which is great, but I'm not done yet. This function is doing two different things: converting the duration into its components, and formatting them. I am going to replace this formatting helper with a simple class that is initialized with a number of milliseconds, and defines toString. This requires rewriting the tests, and might be a waste of time, but the old code will be in git if I need it.

The new Duration class looks a lot like the old duration function:

function Duration(millis){
  this._millis = millis;

  this.hours   = Math.floor(millis / 3600000);
  millis  = millis % 3600000;

  this.minutes = Math.floor(millis / 60000);
  millis  = millis % 60000;

  this.seconds = Math.floor(millis / 1000);
}

Duration.prototype.toString = function(){
  var string = "", 
      h = this.hours, 
      m = this.minutes, 
      s = this.seconds;

  if (h){
    string += h + ":";
  }
  string += (m < 10 ? "0"+m : m) + ":";
  string += (s < 10 ? "0"+s : s);

  return string;
}

As expected, the tests break, but fixing them is no problem, I now create my duration, and then call toString. I renamed the makeDuration helper to inMilliseconds since we now have an actual Duration constructor. Now the tests look like this:

it('formats an empty duration', function(){
  var ms     = inMilliseconds(0,0,0);
    var string = new Duration(ms).toString();

  assert.equal(string, '00:00');
});

I can hop back over the the application and start formatting durations more nicely. Ivy's fnWith lets me bind a formattedDuration to Activity#duration.

function Activity(name, notes, duration){
  //...
  this.formattedDuration = Ivy.fnWith(this,function(duration){
    return new Duration(duration);
  });
}

Any time the duration changes, a new formattedDuration will be generated. The view code just needs to then reference formattedDuration instead of duration:

<td data-bind='text: name'>Item One</td> <td class='duration' data-bind='text: formattedDuration'>00:15</td> 

Now all of the durations are formatted with the nicely tested Duration class. I have introduced some tests, and made the durations look much nicer. Next up, we can look at actually introducing the active state for activities instead of immediately completing them with some random data.

blog comments powered by Disqus
Monkey Small Crow Small