Today I am going to go through the AngularJS’s testability.js file, for two reasons; first, to learn how they approached making the framework testable and second to see if there are any opportunities to clean up the code. Making your code testable appears to be a topic of some debate, one that I do not even understand why the debate exists. Exercise the foresight on how you want to maintain your code in the future to determine the level of pain you wish to experience when making updates when you pick up where you left off, whether that is three months or ten years.

I have removed all the documentation from the source and included it below for our starting point.

The code modifies this by adding a $get method that returns a testability class which has five methods used to work with the framework bindings, models, and location service. I like how they are composing the object in separate function definitions; this allows for easier reading of what each of these methods does without getting lost in a massive function.

In findBindings is where I find an area that we can extract some code into smaller units to help the readability, in particular, this area where we have a lot nesting of code which tends to make it hard to read the intent.

forEach(bindings, function(binding) {
  var dataBinding = angular.element(binding).data('$binding');
  if (dataBinding) {
    forEach(dataBinding, function(bindingName) {
      if (opt_exactMatch) {
        var matcher = new RegExp('(^|\\s)' + escapeForRegexp(expression) + '(\\s|\\||$)');
        if (matcher.test(bindingName)) {
          matches.push(binding);
        }
      } else {
        if (bindingName.indexOf(expression) !== -1) {
          matches.push(binding);
        }
      }
    });
  }
});

All this code uses locally scoped variables, so any extraction is going to require passing in the scoped variable. Our first extraction is one for the exact matches into a small function that determines if we have an exact match.

function isExactMatch(bindingName, expression) {
  var matcher = new RegExp('(^|\\s)' + escapeForRegexp(expression) + '(\\s|\\||$)');
  return matcher.test(bindingName);
}

Do so has reduced one level of nesting, I want to see if I can make this call matches.push(binding) once, the only times we want to add to the match is when either when we are using exact matching, and it matches, or we have a non-exact match. So, with how succinctly I stated what the intent is in my previous sentence, we will now create a couple of methods to help our code read this way. After doing so, I end up with the following:

testability.findBindings = function(element, expression, opt_exactMatch) {
  var bindings = element.getElementsByClassName('ng-binding');
  var matches = getMatchesFromDatabindings(binding, opt_exactMatch, bindingName, expression);
  return matches;
};

And the helper methods:

function getMatchesFromDatabindings(binding, opt_exactMatch, bindingName, expression){
  var matches = [];

  forEach(bindings, function(binding) {
    var dataBinding = angular.element(binding).data('$binding');
    if (dataBinding) {
      matches = addMatchingDatabindingToMatch(dataBinding, opt_exactMatch, bindingName, expression));
    }
  });

  return matches;
}

function addMatchingDatabindingToMatch(dataBinding, opt_exactMatch, bindingName, expression) {
  var matches = [];

  forEach(dataBinding, function(bindingName) {
    if (bindingNameMatches(opt_exactMatch, bindingName, expression)) {
      matches.push(binding);
    }
  }

  return matches;
}

function bindingNameMatches(opt_exactMatch, bindingName, expression) {
  return (opt_exactMatch && isExactMatch(bindingName, expression)) || (isMatch(bindingName, expression));
}

function isMatch(bindingName, expression) {
  return (bindingName.indexOf(expression) !== -1);
}

function isExactMatch(bindingName, expression) {
  var matcher = new RegExp('(^|\\s)' + escapeForRegexp(expression) + '(\\s|\\||$)');
  return matcher.test(bindingName);
}

There is one last area where we can extract a method to help with the readability of the code, and it is in the findModles method.

testability.findModels = function(element, expression, opt_exactMatch) {
  var prefixes = ['ng-', 'data-ng-', 'ng\\:'];
  for (var p = 0; p < prefixes.length; ++p) {
    var attributeEquals = opt_exactMatch ? '=' : '*=';
    var selector = '[' + prefixes[p] + 'model' + attributeEquals + '"' + expression + '"]';
    var elements = element.querySelectorAll(selector);
    if (elements.length) {
      return elements;
    }
  }
};

Here we will extract one helper method that will hide some of the how and replace it with a descriptive function to tell us what it is doing.

function getAllModelsFromElements(element, prefixes, opt_exactMatch, expression) {
  var attributeEquals = opt_exactMatch ? '=' : '*=';
  var selector = '[' + prefixes[p] + 'model' + attributeEquals + '"' + expression + '"]';
  return element.querySelectorAll(selector);
}

Before I show the final version of the code, there is an outstanding question left with the findModels method, that is what it should return when no models are found? The only time this code returns something is when something has been found, otherwise it does not return anything; this could be by design or oversight that could cause an issue with testing.

The final code is below.

So from today's code splitting exercise, we have learned the following:

  1. Author your code to be testable, preferably with TDD, to save the hours of hair pulling when you resume where you had left off.
  2. Extract the details into helper methods that have descriptive names to describe your intent.
  3. It is easy to share scope in a large block of code, this tends to lead to side effects and hard to track bugs.
  4. Be sure all your code paths return something when your method is to return a value.

That is it for today, tune in next week for what lessons we can learn from our next splitting code session.

Remember, if you have a snippet of code that you would like refactored on split code leave a comment below with the code snippet or link to where to obtain it.