TDD with AngularJS

In following post I want to show how one can use TDD approach to drive development from scratch with AngularJS. Although the basic idea of TDD is very simple then it has many variations e.g classical or mockist approach. I have long been in classical camp myself but recently after seeing J.B. Rainsberger's Integration Tests are A Scam I have realized how proper use of Test Doubles can effectively fight against complexity of testing without sacrificing reliability.

The feature I will use is showing user the list of recipients which he has used in the past for making money transfers.

In general I think it is good idea to start implementing every new feature with a high level end-to-end test. However, to keep this post shorter I'm not going to touch that topic here and instead will focus solely on unit testing. I will also leave out backend implementation and assume that I already have an existing server side API. To get current user's recipients we need to make request GET /recipients which returns a JSON array of Recipient objects.

I'm using AngularJS 1.3.4 with ng-annotate and testing stack of Jasmine 2.1 and Karma with the most excellent template testing plugin karma-ng-html2js-preprocessor.

First, I will write a test for the outermost thing which in this case is the router:

describe('Application', function () {  
  //...
  it('routes to recipients list when path is "/recipients"', function() {
    $httpBackend.expectGET('views/recipient-list.html').respond(200);

    $location.path('/recipients');
    $rootScope.$digest();

    $httpBackend.verifyNoOutstandingExpectation();
  });
});

And implementation:

angular  
  .module('angulartddApp', [
    'ngRoute'
  ])
  .config(function ($routeProvider) {
    $routeProvider
      .when('/recipients', {
        templateUrl: 'views/recipient-list.html'
      })
      //other route definitions
      //...
  });

Now that I have some way to get to it in application I will test the recipient-list template:

describe('Recipient List View', function() {  
  'use strict';

  var $compile, scope, $templateCache;

  beforeEach(module('templates'));

  beforeEach(inject(function(_$compile_, $rootScope, _$templateCache_) {
    $compile = _$compile_;
    scope = $rootScope.$new();
    $templateCache = _$templateCache_
  }));

  it('shows list of recipients', function() {
    var template = $templateCache.get('views/recipient-list.html');
    var element = null;

    scope.recipients = {
      list: [
        {name: 'John Doe'},
        {name: 'James Smith'}
      ]
    };

    element = $compile(angular.element(template))(scope);
    scope.$digest();

    expect(element.html()).toContain('John Doe');
    expect(element.html()).toContain('James Smith');
  });
});

And here is the template to make test pass

<div>  
  <div ng-repeat="recipient in recipients.list">
    {{recipient.name}}
  </div>
</div>  

I think that the ability to test your views is essential for TDD. Excluding that layer or covering it with slow end-to-end testing tools like WebDriver makes the whole process significantly harder if not impossible.

Now that I have the template ready it's time to implement fetching some real data from backend so I will add a test for the ViewModel or Controller as it is named in AngularJS for some reason.

describe('Recipient List Controller', function() {

  //beforeEach(...)

  it('asks backend for list of recipients', function () {
    $httpBackend.expectGET('/recipients').respond(200);

    $controller('RecipientListController as recipients', {
      $scope: scope
    });

    $httpBackend.flush();
    $httpBackend.verifyNoOutstandingExpectation();
  });

});

Note that I only test that request is sent to backend. I want to add separate test for response handling to make each test as simple as possible. This will also mean that I have only single reason for the test to fail.

Next stop - implementing the RecipientListController:

(function() {
  'use strict';

  function RecipientListController($http) {
    $http.get('/recipients');
  }

  angular.module('angulartddApp')
    .controller('RecipientListController', RecipientListController);

})();

Now I will add a RecipientListController test to check if it exposes the list of recipients.

it('exposes list of recipients', function() {  
  var johnAndJames = [{name: 'John Doe'}, {name: 'James Smith'}];
  $httpBackend.when('GET').respond(johnAndJames);

  $controller('RecipientListController as recipients', {
    $scope: scope
  });

  $httpBackend.flush();

  expect(scope.recipients.list).toEqual(johnAndJames);
});

Now I can update the controller to make above test pass:

function RecipientListController($http) {  
  var vm = this;

  $http.get('/recipients').success(function(recipients) {
    vm.list = recipients;
  });
}

As per best practices of AngularJS I'm using this inside the controller instead of $scope. In addition to making my controller less framework specific, together with the controllerAs syntax I can be sure that all model data exposed by my controller is a property of an object which is another good thing to have in Angular.

Now that I have the RecipientListController I need to update route definition to tie my recipient list view together with the controller. Should I write a test for it also? I could write a test that looks like this:

  it('uses recipient list controller when showing recipients list', function() {
    $httpBackend.when('GET', /.*/).respond(200);

    $location.path(path);
    $rootScope.$digest();

    expect($route.current.controller).toBe('RecipientListController');
  });

However, the problem with this test is that it doesn't test whether routing actually works correctly. It only tests low level plumbing of the framework. Maybe if there will be some more complex logic in routing like building routes conditionally then TDD may make sense but in current case this test hardly drives anything.

In any case here is the updated route:

$routeProvider
  .when('/', {
    templateUrl: 'views/recipient-list.html',
    controller: 'RecipientListController',
    controllerAs: 'recipients'
  })

I have implemented http request inside the controller. So far logic is quite simple so I could keep it there but I don't like that option. Foremost it violates the Single Responsibility Principle and as the application grows it will make my controller fatter and fatter.

So I will extract http stuff into separate Service. To flesh out its API I will first change the client code - RecipientListController.

function RecipientListController($http, Recipients) {  
    var vm = this;

    Recipients.all().success(function(recipients) {
      vm.list = recipients;
    });
  }

Now I have both controller test failing so will modify them to use Recipients mock instead.

it('asks backend for list of recipients', function () {  
  spyOn(Recipients, 'all').and.returnValue({
    success: function() {}
  });

  $controller('RecipientListController as recipients', {
    $scope: scope
  });

  expect(Recipients.all.calls.count()).toBe(1);
});

it('exposes list of recipients', function() {  
 var johnAndJames = [{name: 'John Doe'}, {name: 'James Smith'}];

  spyOn(Recipients, 'all').and.returnValue({
    success: function(callbackFn) {
      callbackFn(johnAndJames);
    }
  });

  $controller('RecipientListController as recipients', {
    $scope: scope
  });

  expect(scope.recipients.list).toEqual(johnAndJames);
});

I have implemented simple inline stub for httpPromise but in real projects I would prefer to write a small Jasmine extension which uses AngularJS $q internally and avoids stubbing promises inside each test. Then test code would look something like this: spyOn(Recipients, 'all').and.returnAsync(johnAndJames) and spyOn(Recipients, 'all').and.returnAsyncError(errorResponse).

I have collaboration tests using Recipients but no contract tests that would verify that real Recipients actually behaves the way I mock it so I will start adding contract tests.

describe('Recipients', function() {  
  'use strict';

  var Recipients, $httpBackend;

  beforeEach(module('angulartddApp'));

  beforeEach(inject(function(_$httpBackend_, _Recipients_) {
    $httpBackend = _$httpBackend_;
    Recipients = _Recipients_;
  }));

  it('asks backend for list of recipients', function () {
    $httpBackend.expectGET('/recipients').respond(200);

    Recipients.all();
    $httpBackend.flush();

    $httpBackend.verifyNoOutstandingExpectation();
  });

});

Now it's time to implement Recipients:

(function() {
  'use strict';

  function Recipients($http) {
    var self = this;

    self.all = function() {
      return $http.get('/recipients');
    };

  }

  angular.module('angulartddApp')
    .service('Recipients', Recipients);

})();

Now I will add the second contract test for Recipients:

  it('returns list of recipients from backend', function() {
    var recipients;
    var johnAndJames = [{name: 'John Doe'}, {name: 'James Smith'}];
    $httpBackend.when('GET').respond(johnAndJames);

    Recipients.all().success(function(response) {
      recipients = response;
    });
    $httpBackend.flush();

    expect(recipients).toEqual(johnAndJames);
  });

After this I should add more tests for special cases and error handling but these will follow same process I have already outlined - first, add new test/implementation for the outermost layer (temporarily ignoring any application architectural principles) and then work down to the innermost layer moving logic to the places where it belongs to.