Jasmine Testing Angular Controller with Service(s)

My experience with testing in Angular 1.x is with Jasmine and using Karma as a test runner. When I first started I struggled to figure out how to test a controller that had a dependency on a service that makes a call to an API. Below is an example of a service, controller and controller_spec as an example of how to structure a Jasmine test for a Angular controller with a service(s).

Example Service #1:


angular.module('ActivityApp').service('PersonService', [
  '$resource',
  function ($resource) {
    'use strict';

    var Person = $resource('', null, {
      get: {
        method: 'GET',
        url: '/apis/person'
      }
    }));

    return Person;
  }
]);

Example Service #2:


angular.module('ActivityApp').service('ActivityService', [
  '$resource',
  function ($resource) {
    'use strict';

    var Activity = $resource('', null, {
      query: {
        method: 'GET',
        url: '/apis/activity'
      }
    }));

    return Activity;
  }
]);

Example Controller:


angular.module('ActivityApp').controller('PersonController', [
  '$scope',
  'PersonService',
  'ActivityService',
  function ($scope, PersonService, ActivityService) {
    'use strict';

    $scope.person = PersonService.get();
    $scope.activity = ActivityService.query();
    $scope.formData = {};

    $scope.resolveAll = function (response) {
      // response is an array of both resolved promises
    };

    $scope.save = function () {
      PersonService.save($scope.formData);
    };

    $q.all([$scope.person.$promise, $scope.activity.$promise]).then($scope.resolveAll);
  }]);

Example Controller Spec:


describe('PersonController', function() {

  var $controller,
      $scope,
      PersonService,
      ActivityService;

  beforeEach(module('ActivityApp'));

  beforeEach(function () {
    PersonService = jasmine.createSpyObj('PersonService', [
      'get',
      'save'
    ]);

    ActivityService = jasmine.createSpyObj('ActivityService', [
      'query'
    ]);

    module(function ($provide) {
      $provide.value('PersonService', PersonService);
      $provide.value('ActivityService', ActivityService);
    });
  });

  beforeEach(inject(function(_$controller_, $rootScope) {
    $scope = $rootScope.$new();

    $controller = _$controller_('PersonController', {
      $scope: $scope
    });
  }));

  it('should be defined and call services', function() {
    expect($controller).toBeDefined();
    expect(PersonService.get).toHaveBeenCalled();
    expect(ActivityService.query).toHaveBeenCalled();
  });

  describe('$scope.save', function() {
    it('save formData to PersonService', function() {
      $scope.formData = { test: 'test' };
      $scope.save();
      expect(PersonService.save).toHaveBeenCalledWith({ test: 'test' });
    });
  });
});

The spec for the PersonController above asserts that when it is initiated, it will call both the PersonService and the ActivityService with their respective methods. Additionally, the $scope.save method will call the PersonService save method with the $scope.formData object.

It’s not the concern of the controller for what the service does. If the rest of your controller depends on the return value from the service, you can mock that out and create a promise for the other functions to react to.

Example 2 Controller Spec:


describe('PersonController', function() {

  var $controller,
      $scope,
      PersonService,
      ActivityService
      PersonGetPromise,
      ActivityQueryPromise;

  beforeEach(module('ActivityApp'));

  beforeEach(function () {
    PersonService = jasmine.createSpyObj('PersonService', [
      'get',
      'save'
    ]);

    ActivityService = jasmine.createSpyObj('ActivityService', [
      'query'
    ]);

    module(function ($provide) {
      $provide.value('PersonService', PersonService);
      $provide.value('ActivityService', ActivityService);
    });
  });

  beforeEach(inject(function(_$controller_, $rootScope, $q) {
    PersonGetPromise = $q.defer();
    ActivityQueryPromise = $q.defer();

    PersonService.get.and.returnValue({ $promise: PersonGetPromise.promise });
    ActivityService.query.and.returnValue({ $promise: ActivityQueryPromise.promise });

    PersonGetPromise.resolve('MOCK DATA');
    ActivityQueryPromise.resolve('MOCK DATA');

    $scope = $rootScope.$new();

    $controller = _$controller_('PersonController', {
      $scope: $scope
    });
  }));

  it('should be defined and call services', function() {
    expect($controller).toBeDefined();
    expect(PersonService.get).toHaveBeenCalled();
    expect(ActivityService.query).toHaveBeenCalled();
  });

  describe('$scope.save', function() {
    it('save formData to PersonService', function() {
      $scope.formData = { test: 'test' };
      $scope.save();
      expect(PersonService.save).toHaveBeenCalledWith({ test: 'test' });
    });
  });
});

Now both promises have resolved without making a call to the external API. Instead of resolving the promises in the before each, you could also resolve then in the it() block or in another beforeEach() in a lower describe() block. This would allow you to create tests with different return values or with a rejected promise to test error cases.

posted in: Front End Development
tagged: , ,

Comments are closed