Unit Testing Angular Views To Avoid the Integrated Test Scam

At a recent engineering gathering we watched the Integrated Tests are A Scam talk by J.B. Rainsberger. Along with that was discussion about ReactJS’s approach to testing with Jest. With Jest, views are React components that can be tested in isolation without browsers or servers. Both of these topics provoked a great deal of discussion about how we can eliminate integrated tests and lean more on unit tests.

In this blog post I will be focusing on how to avoid integrated tests in client-side Angular.JS applications.

Testing a View

For example, here is a simple dropdown box with a controller that supplies the currently logged-in user.

<li class="dropdown">  
  <a id="email" href="#" data-toggle="dropdown" class="dropdown-toggle">
    {{ user.email || user.name || "Anon" }}
  </a>
  <ul class="dropdown-menu">
    <li>
      <a id="logout" data-ng-click="logout()">
        Logout
      </a>
    </li>
  </ul>
</li>  

What are the key scenarios to be tested here? I will cover the following:

  • It should display the user email when the user has an email.
  • It should display the user name when the user has no email.
  • It should display a default user name when the user has no email or name.
  • It should logout when the logout button is clicked in the dropdown menu.

What is the best way to write a test for each of these scenarios? According to the Angular.js community, the common answer is to write an integrated test, typically using Protractor.

However, integrated tests have the following drawbacks associated with them:

  • They force unrelated systems into the test, thereby creating room for test failures unrelated to the system under test.
  • They are unopinionated about the design of the view, so there is no pressure for it to be broken up into smaller views, controllers, or directives.
  • Over time, integrated tests can become slow and hard to maintain.
  • They are fragile when changes occur in the application.

The above four integrated tests involve the real implementation of the controller for the corresponding view, thereby leaving it exposed to bugs in that system. The fourth scenario includes the logout interaction, which could involve page transitions, database calls, updating localStorage user models, and triggering analytics events. The basic correctness of the view should not require brittle tests.

Unit Testing a View

I was inspired by the Integrated Tests are A Scam talk to defy conventional wisdom and unit test these scenarios. Unit testing views can be done in Angular.js and avoids the integrated test problems mentioned earlier. These tests validate the view independent of other systems, and can be run both in parallel and in memory without spinning up the server or database. Pressure is put on the design of the view by the tests, which encourages the creation of new views and directives.

An interactive example with all four scenarios is on plunkr; however, the approach can be explained by the following snippets:

// Get html template from cache
beforeEach(inject(function($templateCache) {  
  viewHtml = $templateCache.get("some/valid/templateUrl");
}));
// Setup stubbed scope and create dropdownElement.
var $rootScope;  
var $scope;  
var $compile;  
var dropdownElement;

beforeEach(inject(function(_$compile_, _$rootScope_){  
  $compile = _$compile_;
  $rootScope = _$rootScope_;

  $scope = $rootScope.$new();
  $scope.user = {};
  $scope.logout = sinon.stub();
  dropdownElement = angular.element(viewHtml);
}));
// Compile element, digest cycle for stubbed scope, verify expectations
it("should display the user email when the user has an email.", function () {  
  $scope.user.email = "cody@trov.com";
  compileElement();

  var email = dropdownElement.find("#email");

  expect(email.text().trim()).toBe($scope.user.email);
});

function compileElement() {  
  $compile(dropdownElement)($scope);
  $rootScope.$digest();
}

Conclusion

Comparing the two approaches demonstrates that Angular.JS developers can save time unit testing their views by avoiding duplicate tests, minimizing scaling concerns, and providing isolated failures. In addition, unit tests have an added benefit of putting refactor pressure on view design, thus reducing complexity. For us this is the best approach to unit testing views, and supports our overall goal of avoiding the integrated test scam.

Further Reading