Friday, January 6, 2012

IIFEs, Closures, Unit Testing and Privacy

One of the toughest decisions within JavaScript code design is the choice between testability and privacy. It's definitely one of the most frustrating aspects of JavaScript for me personally (and that is definitely saying something). This choice can lead to a less than ideal design if unit testing is important to you... which it should be.

Lets start with an example to see how this choice can affect design. We are going to create an object that will represent a basic parallelogram that requires as input the measurements of two adjacent sides and an angle. Assuming the second measurement is the base, the object will internally compute the height and area of the parallelogram from the inputs on object creation exposing the original measurements and the two new computed values.

A typical design pattern for an object in JavaScript is to use an IIFE (Immediately Invoked Function Expression) to create a closure so we don't pollute the global namespace. A simple design (we aren't concerning ourselves with optimizations for this discussion) could look like:
(function( win, undef ) {
  // Global namespace object name
  var objName = 'myAwesomeParallelogram';

  // Compute the height of a parallelogram where length is the adjacent side
  // of the base and the angle is acute in radians length * sin(angle)
  function height( length, angle ) {
    return length * Math.sin( Math.min( angle, 180 - angle ) * Math.PI/180 );
  }

  // Compute the area of a parallelogram b * h
  function area( base, height ) {
    return base * height;
  }

  var obj = function( sideA, sideB, angle ) {
    if ( angle < 0 || angle > 180 ) {
      throw new SyntaxError('Angle is not between 0 and 180 degrees');
    }

    this.sideA = sideA;
    this.sideB = sideB;
    this.angle = angle;

    this.height = height( sideA, angle );
    this.area = area( sideB, this.height );
  };

  win[ objName ] = obj;
})(window);
Sample usage:
> var p = new myAwesomeParallelagram( 15, 12, 45);
undefined
> p.area
127.27922061357853
> p.height
10.606601717798211
Great! But aside from testing the object itself, how would we be able to test if height or area work correctly? How would we test the code flow? We could redesign our code to expose these functions and we could likewise include some logging calls to console to show the flow. But, is this what we want in production? Is it necessary?

There have been a few clever ways of handling this dilemma (I know I rolled a few of my own over the years) but Mr. Douglas Crockford (the author of JavaScript: The Good Parts and JSLint) has just released a new tool to help end the Sophie's choice between good design/security and testability. It's called JSDev and it allows you to include specially formated comments that can be transformed into development unit testing code and then removed through normal minification processes.

To use the new tool we first need to download the raw file (or use git) and compile it locally for a CLI.
$ curl 'https://raw.github.com/douglascrockford/JSDev/master/jsdev.c' -o jsdev.c && gcc jsdev.c -o jsdev
Now that we have a compiled version of jsdev we can edit our original object code to include development only code for unit testing.
(function( win, undef ) {
  // Global namespace object name
  var objName = 'myAwesomeParallelogram';

  // Compute the height of a parallelogram where length is the adjacent side
  // of the base and the angle is acute in radians length * sin(angle)
  function height( length, angle ) {
    return length * Math.sin( Math.min( angle, 180 - angle ) * Math.PI/180 );
  }

  // Compute the area of a parallelogram b * h
  function area( base, height ) {
    return base * height;
  }

  var obj = function( sideA, sideB, angle ) {
    if ( angle < 0 || angle > 180 ) {
      throw new SyntaxError('Angle is not between 0 and 180 degrees');
    }

    this.sideA = sideA;
    this.sideB = sideB;
    this.angle = angle;

    this.height = height( sideA, angle );
    this.area = area( sideB, this.height );
  };

  // JSDev code comments
  /*dev
    obj.height = height;
    obj.area = area;
  */
 
  win[ objName ] = obj;
})(window);
Now if we process it through or handy dandy new tool:
$ ./jsdev dev -comment "Development Version" < input.js > output-dev.js
The new file, output-dev.js looks like:
// Development Version
(function( win, undef ) {
  // Global namespace object name
  var objName = 'myAwesomeParallelogram';

  // Compute the height of a parallelogram where length is the adjacent side
  // of the base and the angle is acute in radians length * sin(angle)
  function height( length, angle ) {
    return length * Math.sin( Math.min( angle, 180 - angle ) * Math.PI/180 );
  }

  // Compute the area of a parallelogram b * h
  function area( base, height ) {
    return base * height;
  }

  var obj = function( sideA, sideB, angle ) {
    if ( angle < 0 || angle > 180 ) {
      throw new SyntaxError('Angle is not between 0 and 180 degrees');
    }

    this.sideA = sideA;
    this.sideB = sideB;
    this.angle = angle;

    this.height = height( sideA, angle );
    this.area = area( sideB, this.height );
  };

  // JSDev code comments
  {
    obj.height = height;
    obj.area = area;
  }
 
  win[ objName ] = obj;
})(window);
Before was continue there are a few things to notice between the input files, the command and the output file:
  • The comment from the CLI is now a header comment in the output file. This is handy to always include the type of output file in the file.
  • The other arguments for the CLI specify which multi line comments that match the format /*<argument> <code>*/ to include in the output file. This is handy if you have different testing levels or needs. Note that there cannot be a space between the opening comment notation and the argument.
  • Single line comment notations ("//") are unaffected by the jsdev tool as well as multi line comments that don't match the specified format above.
Now lets see if we can access our development code private functions using a console.
> myAwesomeParallelogram.area
  function area( base, height ) {
    return base * height;
  }
> myAwesomeParallelogram.height
  function height( length, angle ) {
    return length * Math.sin( Math.min( angle, 180 - angle ) * Math.PI/180 );
  }
And when we minify the original code we get this (using JSMin for this example):
(function(win,undef){var objName='myAwesomeParallelogram';function height(length,angle){return length*Math.sin(Math.min(angle,180-angle)*Math.PI/180);}
function area(base,height){return base*height;}
var obj=function(sideA,sideB,angle){if(angle<0||angle>180){throw new SyntaxError('Angle is not between 0 and 180 degrees');}
this.sideA=sideA;this.sideB=sideB;this.angle=angle;this.height=height(sideA,angle);this.area=area(sideB,this.height);};win[objName]=obj;})(window);
We no longer need to sacrifice our design and security for testability. Is it the most elegant solution? Maybe not, but it works well and can be easily added to a build process for testing and will not impact your current build process for production.

For more information and more usage info, please refer to the README file on github.

27 comments: