How to Test an Asynchronous Call Made by an AngularJS Directive

by Larry Spencer Saturday, May 17, 2014 10:05 PM
In an AngularJS application I'm developing at work, we try to let our pages load as quickly as possible, showing the user each piece of content as it becomes available. Why make her wait for a dropdown list to populate when she could get started entering something in a textbox?

While a part of the page is waiting for data, we can hide it with a spinner or display a message. The simple AngularJS application below shows how a directive can display differently while it is waiting for an async call to complete. The directive displays "Waiting..." during the wait and "Done!" when the wait is over.


angular.module('app',[])

.service('AsyncService', function ($q) {
  
  // Like Angular's $http.get, this function returns a promise.
  this.promiseToDoSomething = function() {
    return $q.when('Promise fulfilled!');
  };
})

.directive('directiveUsingAsyncService', function() {
  
  return {
    restrict: 'E',
    
    scope: {},
    
    controller: ['$scope', 'AsyncService', function ($scope, asyncService) {
      
      // The text to which this directive's <p> element binds changes from 
      // "Waiting..." to "Done!" when the async operation completes.
      $scope.text = 'Waiting...';
      
      // Like Angular's $http.get, this function returns a promise.
      asyncService.promiseToDoSomething().then( function() {
        $scope.text = 'Done!';
      });
    }],
    
    template: '<p>{{text}}</p>'
  };
});
Simple enough, but how can we test the behavior?

Here's one way, using Jasmine. The trick is to replace the call to the async method with a Jasmine spy that returns an AngularJS promise (just like the real method does). Then, we can resolve the promise "by hand" at the appropriate time.
 

describe('directiveUsingAsyncService', function(){
  
  var elementUnderTest;
  var deferredResolution;
  var parentScope;
  
  beforeEach(module('app'));
  
  beforeEach(angular.mock.inject(function($compile, $rootScope, $q, AsyncService ) {

    // $q.defer() returns an object that packages a promise together with
    // methods to resolve or reject it. We will use this variable to manually
    // resolve the promise at the right moment.
    deferredResolution = $q.defer();
   
    // Replace the AsyncService.promiseToDoSomething() with a spy that
    // returns the promise inside our $q.defer() object.
    spyOn(AsyncService,'promiseToDoSomething')
      .andReturn(deferredResolution.promise); 
     
    // Add the directive we want to test to the DOM.
    elementUnderTest = angular.element('<directive-using-async-service></directive-using-async-service>');
    parentScope = $rootScope.$new();
    $compile(elementUnderTest)(parentScope);
    $('body').append(elementUnderTest);    
    parentScope.$apply();
  }));
  
  afterEach(function() {
    $(elementUnderTest).remove();  
  });
  
  var getCurrentText = function () {
    return $(elementUnderTest).text();
  }
  
  it('says "Waiting..." before the async method completes', function() {
    
    // We have not yet resolved deferToResolveCallToAsyncService.promise,
    // so the directive should behave as if the service call has not completed.
    expect(getCurrentText()).toBe("Waiting...");
  });
  
  it('says "Done waiting!" after the async method completes', function() {
    
    // Pretend that the AsyncService.promiseToDoSomething has completed.
    deferredResolution.resolve("Here's your data!");
    // Need to run a digest cycle. Angular's asynchronous methods such as
    // $http.get generally do this for you, but our method does no such favor.
    parentScope.$apply();
    
    expect(getCurrentText()).toBe("Done!");
  });
});

Here's the whole thing in Plunker if you'd care to play around with it.

Tags: , ,

All

About the Author

Larry Spencer

Larry Spencer develops software with the Microsoft .NET Framework for ScerIS, a document-management company in Sudbury, MA.