← all articles

Unit testing an AngularJS factory method returning a promise

Simon Holmes | 8 July 2016 | AngularJS | Testing

In this article we're going to talk about how to write a Jasmine unit test to validate the values of a promise returned from an Angular factory. That sounds like a complex introduction! So here's a breakdown of the scenario:

  1. You have an Angular factory with a method that returns a promise
  2. The promise can return different values depending on what happens in the factory method
  3. The factory method accepts an argument, the value can impact the outcome
  4. You want to unit test the factory
  5. You want to validate that the correct value is returned from the promise in different situations

A real-world scenario?

This article came about because I had a real need to do this. In my scenario I had a factory method that called an external asynchronous service, and returned a promise. But I only wanted to call that service if the data passed into the factory was correct.

If the incoming data wasn't correct I wanted to return immediately, passing an error notification back. Naturally this had to be done through a promise, as the success response was a promise.

As part of my testing I wanted to show that the factory would resolve the promise with some "error" data if the arguments passed in were incorrect.

The AngularJS application

We're not going to use my production code here as there's a lot of complexity surrounding it, we'll stick with a simple example to illustrate the point.

The full source code for this article is available on GitHub

The Angular factory returning a promise

This snippet shows a simple factory called promiseFactory that has a method called getResponse. This method accepts a value val and returns a deferred promise. If val === true then the promise resolves with a "true" message, otherwise it returns with a "false" message.

function promiseFactory($q) {

  function getResponse(val) {
    var deferred = $q.defer();

    if (val === true) {
      deferred.resolve('This is totally true');
    } else {
      deferred.resolve('This is clearly false');
    }

    return deferred.promise;
  }

  return {
    getResponse : getResponse
  };

}

If you really want to demonstrate the asynchronous nature of promises you could wrap each of the deferred.resolve lines in a callback but we're keeping it simple here.

The controller

Naturally we need a controller to call the factory method. Again we'll keep this simple.

In the controller we have:

  • vm.promiseValue to store the returned value
  • A vm.onClick method that takes a value and passes it to promiseFactory.getResponse
  • The promise resolve block (.then) which updates the value of vm.promiseValue
angular
.module('fst')
.controller('fstCtrl', function (promiseFactory) {

  var vm = this;

  vm.promiseValue = 'undefined';

  vm.onClick = function(val) {
    promiseFactory
      .getResponse(val)
      .then(function(res) {
        vm.promiseValue = res;
      });
  };

});

The HTML

Finally of course we'll also have a simple HTML page, with buttons to call the vm.onClick method and a binding for vm.promiseValue.

<body ng-controller="fstCtrl as vm">

  <h1>Click a button</h1>
  <button ng-click="vm.onClick(true)">Resolve true</button>
  <button ng-click="vm.onClick(false)">Resolve false</button>
  <p>Promise value: <em>{{ vm.promiseValue }}</em></p>

</body>

When it's all running it looks like this:

You can see this for yourself by running it in Plunker or get the full source code for this article from GitHub

Unit testing the promise

For the unit tests we are using Jasmine and Karma. If you've got to this post I pretty much assume you've already got both of those things working!

Inject the rootScope and factory

When unit testing a factory you need to inject the factory itself before the tests run, so that they can use it. This is standard practice.

In order to test with promises we also need to inject the rootScope and use that to generate a new scope. We'll see why soon.

beforeEach(inject(function(_$rootScope_, _promiseFactory_){
  // The injector unwraps the underscores from around the parameter names when matching

  // Generate a new scope
  scope = _$rootScope_.$new();
  // Expose the factory to the tests
  promiseFactory = _promiseFactory_;
}));

The base unit test

Here's a very simple unit test spec that you might reasonably expect to work.

We're calling the getResponse method of the factory and passing the value true. We've chained a .then on to listen for the promise, and then run the expectation.

it('should resolve promise when passed true', function() {
  promiseFactory
    .getResponse(true)
    .then(function(res) {
      expect(res).toBe('This is totally true');
    });
});

Makes sense right? Yes, but this won't work. There are a couple of things to do.

Make the promise resolve

First of all the promise in the factory doesn't run. Promises using $q are processed on each run of Angular's digest cycle, but this doesn't run in the testing environment. You need to manually run the digest cycle to make the promise resolve.

This is as simple as adding in a scope.$digest(); line add the end of the test.

it('should resolve promise when passed true', function() {
  promiseFactory
    .getResponse(true)
    .then(function(res) {
      expect(res).toBe('This is totally true');
    });
  scope.$digest();
});

This will run Angular's digest cycle so the promise will then resolve. But still the test won't work.

Make the test asynchronous

This test runs in a linear synchronous fashion, it runs from top to bottom and then moves onto the next one. But promises by their very nature are asynchronous.

We need to say that the test is only complete once the promise has resolved, and the expectation has run.

Jasmine offers a very simple way to make a test asynchronous. Into the test function we can just pass a done argument, which is actually a function. Calling this function tells Jasmine that we've finished this test.

it('should resolve promise when passed true', function(done) {
  promiseFactory
    .getResponse(true)
    .then(function(res) {
      expect(res).toBe('This is totally true');
      done();
    });
  scope.$digest();
});

Notice there that we pass in done at the top and then call done() after the expectation.

And that is how to unit test the value returned by a promise in a factory!

Get the code and try it yourself

This post has omitted some of the boiler-plate code in order to focus on the important aspects.

The full source code is available on Github.

When you have cloned the repo, to view the "app" put the repo on a webserver and look at app/index.html

To run the unit tests:

  1. Clone the repo
  2. Run npm install
  3. Run npm test

If you don't already have it already you'll also need to install the Karma command line interface by running:

npm install -g karma-cli

Free email mini-course on
Full Stack development

Sign up now to receive a free email mini-course covering the MEAN stack and more. Also be the first to know when we release new courses and videos.