Sunday 22 December 2013

Mocking promises in Angular JS Controller Tests

In a typical Angular JS application, we wrap the calls to backend services in a custom Angular JS service and return a promise from the method in the service to the calling component. For instance, say the calling component is a controller. While testing the controller, we create a mocked service instance with methods replaced by spies. As the original method in the service returns a promise containing the result from executing the backend API, the spy should return a mocked promise with a dummy result.

A few months back, when I was learning Angular and blogging my learning, I did blog posts on unit testing controllers using Jasmine and also on unit testing controllers using QUnit. Back then, I used to use spyOn().andCallThrough() on all methods in the service and used $httpBackend to avoid calling the backend APIs from the service. With time, I understood that this approach is not good as the controller still depends on the logic written inside the service. In this post, we will see how to return mock promises from the spies to isolate controller from the service.

Following are the service and the controller we will be using in this post:

var app = angular.module("myApp", []);
app.factory('dataSvc', function($http, $q){
    var basePath="api/books";
    getAllBooks = function(){
        var deferred = $q.defer();
 $http.get(basePath).success(function(data){
            deferred.resolve(data);
        }).error(function(err){
          deferred.reject("service failed!");
        });
        return deferred.promise;
     };
  
     return{
         getAllBooks:getAllBooks
     };
});

app.controller('HomeController', function($scope, $window, dataSvc){
   function initialize(){
       dataSvc.getAllBooks().then(function(data){
           $scope.books = data;
       }, function(msg){
          $window.alert(msg);
       });
   }

  initialize();
});

Let’s create a spy for the service. This is a bit tricky, as we need to force the promise to pass or fail based on a condition. At the same time, it is simple as $q provides the ready methods when and reject to make our job easier. Following is the spy for getAllBooks method:
var succeedPromise;
spyOn(booksDataSvc, "getAllBooks")
    .andCallFake(function(){
        if (succeedPromise) {
            return $q.when(booksData);
        }
        else{
            return $q.reject("Something went wrong");
        }
    });

The fake implementation of getAllBooks passes if value of the field succeedPromise is set to true, otherwise it fails. We need to manipulate this field in the test cases.

In the test cases, we need to call scope.$digest before checking the expectations, as the promise is triggered from a spy method, which is in non-angular world.

Following test case checks if the books object is set to some value when the promise passes.

it('Should call getAllBooks on creating controller', function(){
    succeedPromise = true;
    createController();
    homeCtrlScope.$digest();
    expect(booksDataSvc.getAllBooks).toHaveBeenCalled();
    expect(homeCtrlScope.books.length).not.toBe(0);
  });

The promise can be forced to fail by just setting succeedPromise to false in the test case. Following test case demonstrates it:
it('Should alert a message when service fails', function(){
    succeedPromise = false;
    createController();
    homeCtrlScope.$digest();
    expect(booksDataSvc.getAllBooks).toHaveBeenCalled();
    expect(windowMock.msg).not.toBe("");
  });

The complete sample is available on plnkr: http://plnkr.co/edit/xD9IPb6TRduAUwRGbIIG

Happy coding!

1 comment:

Note: only a member of this blog may post a comment.