Unit testing an AngularJS directive's private functions.

by Richard — 4 minutes

As we all know Javascript gives us the awesome ability to create functions inside functions. This allows us to create private functions which support the main function. It is also something we do often when creating object functions. This structure is used by angular for the creation of providers and directives alike.

Every once in a while I personally come to a point where I would like to test these private functions. This is especially true for use cases in Angular such as directives.I'd like to be able to run unit tests for a directive's private functions, but I'd like to do this without having to make them public. The way I do this is by using a concept called reflection. This process actually described by Bob Gravelle in his post 'Accessing Private functions in Javascript' actually exposes the private functions by using the toString method of a function. Before I go into specifics let me say that this article should only be used as an approach  for unit testing. There is a good reason for keeping private functions private and using this concept for application code may very well introduce interesting side effects. That being said let's go into details. In order for us to use this concept we'll need to make some slight changes to our directive. Normally we would declare our Directive Definition Object (DDO) and directly return it. As below:

return {
  restrict: 'E'
  ...
}

This creates a problem when we try to expose the directive code. As keeping this would end up in an evaluated return statement and the corresponding DDO. So in order to get our private functions we'll split the declaration and return statements. As below:

var instance = {
  restrict: 'E',
  ...
}

return instance;

Now our directive is ready to be read for exposure. In order to expose the directive's contents we need to create a function that does so. This will be i slightly modified version of Bob Gravelle's reflection object..

var Reflection = {};

Reflection.createExposedInstance = function (objectConstructor) {
  // get the functions as a string
  var objectAsString = objectConstructor.toString();
  var aPrivateFunctions = objectAsString.match(/function\s*?(\w.*?)\(/g);

  // To expose the private functions, we create
  // a new function that goes trough the functions string
  // we could have done all string parsing in this class and
  // only associate the functions directly with string
  // manipulation here and not inside the new class,
  // but then we would have to expose the functions as string
  // in the code, which could lead to problems in the eval since
  // string might have semicolons, line breaks etc.
  var funcString =
    "new (" +
    objectAsString.substring(0, objectAsString.length - 1) +
    ";" +
    "this._privates = {};\n" +
    "this._initPrivates = function(pf) {" +
    "  this._privates = {};" +
    "  for (var i = 0, ii = pf.length; i < ii; i++)" +
    "  {" +
    "    var fn = pf[i].replace(/(function\\s+)/, '').replace('(', '');" +
    "    try { " +
    "      this._privates[fn] = eval(fn);" +
    "    } catch (e) {" +
    "      if (e.name == 'ReferenceError') { continue; }" +
    "      else { throw e; }" +
    "    }" +
    "  }" +
    "}" +
    "\n\n})()";

  var instance = eval(funcString.replace("return instance;", ""));
  instance._initPrivates(aPrivateFunctions);

  // delete the initiation functions
  delete instance._initPrivates;

  return instance;
};

As you can see in the highlighted line (18) i effectively remove the "return instance;" code from the function body. This means that when we evaluate the code we get back the function rather then it's return object. Thus allowing us to read the private functions. So when we call the following line we end up wit a variable myInputDirectiveExposed which contains an object _privates containing the private functions. This object can then be used to unit test the individual private functions.

var myInputDirectiveExposed = Reflection.createExposedInstance(
  myInputDirective,
);

In order to play around with this system i've created a plnkr. Please feel free to play around with it.

meerdivotion

Cases

Blogs

Event