2014-09-11

AngularJS - how to test $http calls (with tests example)


AngularJS templates no longer automatically unwrap promises (AngularJS >= 1.2). See https://code.angularjs.org/1.2.24/docs/guide/migration#templates-no-longer-automatically-unwrap-promises

This feature is now deprecated but if you absolutely need it, it can be reenabled for now via the $parseProvider.unwrapPromises(true) API.

In this post we will see how to:
  • separate data lookup from controllers logic. It is way way better implement $http calls into dedicated angular services for separation of concerns, good design and software testability
  • how to test $http calls with mocks
This is just a reminder for me but I hope other developers will save time if they don't figure out why their code does not how expected.

Meaningful code in bold, feedback please!

Controller

Controller's code

scripts/controllers/car.js:
'use strict';

angular.module('angularApp')
  .controller('CarCtrl', ['$scope', '$routeParams', 'carFactory', function ($scope, $routeParams, carFactory) {

    // route params (example: )
    $scope.params = $routeParams;

    carFactory.get($scope.params.carId)
      .success(function(data) {
        $scope.data = data;
      });
  }]);

Controller's test code

test/spec/controllers/car.js:
'use strict';

describe('Controller: CarCtrl', function () {

  // load the controller's module
  beforeEach(module('angularApp'));

  var CarCtrl,
    scope,
    success;

  // Initialize the controller and a mock scope
  beforeEach(inject(function ($controller, $rootScope, $q) {

    var promise = $q.when({id: '1', title: 'Audi'});
    promise.success = function(fn) {
      promise.then(function(response) {
        fn(response);
      });
      return promise;
    };

    scope = $rootScope.$new();
    CarCtrl = $controller('CarCtrl', {
      $scope: scope,
      $routeParams: {carId: 'audi'},
      carFactory: {get: function () {return promise;}}
    });
  }));


  it('should have routeParams into params', function () {
    expect(scope.params.carId).toBe('audi');
  });

  it('should have carFactory data', function () {
    scope.$digest();
    expect(!!scope.data).toBe(true);
  });
});

Service

Service code

The base url of my remote endpoint can be injected with:
.value('apiPrefix', 'http://localhost:3001/')
if you are using a service and it is a value shared with other components.
Otherwise can use a configurable service (provider).

Using value or a configurable service helps you to mock things injecting different endpoint urls during development.

scripts/services/carfactory.js):
'use strict';

angular.module('angularApp')
  .factory('carFactory', ['$http', 'apiPrefix', function ($http, apiPrefix) {
    // Service logic
    // ...
    var base = apiPrefix ? apiPrefix : '';

    // Public API here
    return {
      get: function (carId) {
        var promise;

        promise = $http.jsonp(base + 'cars/' + carId + '?callback=JSON_CALLBACK');
        return promise;
      }
    };
  }]);

Service test code

test/spec/services/carfactory.js:
'use strict';

describe('Service: carFactory', function () {

  // load the service's module
  beforeEach(module('angularApp'));

  // instantiate service
  var carFactory, $httpBackend, apiPrefix;
  beforeEach(inject(function (_carFactory_, _$httpBackend_, _apiPrefix_) {
    carFactory = _carFactory_;
    $httpBackend = _$httpBackend_;
    apiPrefix = _apiPrefix_;

    $httpBackend.whenJSONP(apiPrefix + 'cars/audi?callback=JSON_CALLBACK').respond({
      id: 'audi',
      title: 'Audi'
    });
  }));

  afterEach(function() {
    $httpBackend.verifyNoOutstandingExpectation();
    $httpBackend.verifyNoOutstandingRequest();
  });

  it('should do something', function () {
    expect(!!carFactory).toBe(true);
  });

  it('test get data', function () {
    var data, promise;
    promise = carFactory.get('audi');
    promise.success(function(res) {
      data = res;
    });
    $httpBackend.flush();

    expect(data.title).toBe('Audi');
  });

});

No comments:

Post a Comment