Testing Asynchronous Code With AngularJS and Jasmine

One of the nice features about Angular is the testability. The dependency injection makes it easy to test components in isolation and stub out dependencies where necessary. However, it is not immediately obvious how to test asynchronous code. To illustrate how this works I am going to create an example (with asynchronous code) and test this using Jasmine.

Code Example

I am going to create a list of users and allow the user to remove these. The html code might look something like this:

<div ng-app="example">
  <ul ng-controller="UsersController as ctrl">
    <li ng-repeat="user in users">
      <span class="user-name">{{ user.name }}</span>
      <a href="" ng-click="ctrl.deleteUser(user)">Remove</a>
    </li>
  </ul>
</div>

The controller in this case could delegate the actual http call to a service.

app.controller "UsersController", ($scope, userService) ->
  $scope.users = [
    { id: 1, name: "Bob" },
    { id: 2, name: "Joe" },
    { id: 3, name: "Steve" }
  ]

  @deleteUser = (user) ->
    userService
      .delete(user)
      .then ->
        i = $scope.users.indexOf(user)
        $scope.users.splice(i, 1)

  @

(Notice that this code is problematic - no error handler is attached to the promise.)

So how do we go about testing this? There are actually 2 steps that we’re trying to test here. First, when we call this function we can test that it calls userService.delete with the given user. Second, when this promise is resolved we want to test that the user is removed from the original list of users.

Setup the test

If you have never tested an angular controller with Jasmine it might seem like you need to do a large amount of setup, but it’s actually pretty easy and after a while it becomes second nature.

In order to create the controller in our test we need to specify a scope for the controller to use. In the regular application flow angular will supply the (parent) scope, but in the test we always need to specify the scope. We do this by creating a new scope with $rootScope.$new().

describe "UsersController", ->
  ctrl = null
  scope = null

  beforeEach ->
    module "example"

    inject ($controller, $rootScope) ->
      scope = $rootScope.$new()
      ctrl = $controller("UsersController", $scope: scope)

This is all in order to create the controller. The other dependency that we want to stub out is the userService. I am therefore going to inject this service and store a reference to it - I will stub out the delete function later on.

describe "UsersController", ->
  ctrl = null
  scope = null
  userService = null

  beforeEach ->
    module "example"

    inject ($controller, $rootScope, _userService_) ->
      userService = _userService_
      scope = $rootScope.$new()
      ctrl = $controller("UsersController", $scope: scope)

Notice that I am injecting the userService as _userService_ - this is a convenience trick that we can use in order to assign the injected class to a variable of the same name.

Write the actual test

Let’s tackle the first step - checking that the userService is actually called with the specified user.

describe "#deleteUser", ->
  beforeEach ->
    scope.users = ["Dave", "Steve", "Bob", "Joe"]
    spyOn(userService, "delete")

    ctrl.deleteUser("Bob")

  it "should delete the user", ->
    expect(userService.delete).toHaveBeenCalledWith("Bob")

If we run this test, we will get the following error:

TypeError: Cannot read property ‘then’ of undefined

This makes perfect sense if we look at our implementation code. The implementation is expecting userService.delete to return a promise, but we are simply returning null (the default behavior of spyOn is to return null).

This is where we need to start handling the asynchronous nature of this code. I am going to inject the $q service and create a promise that we can return in our spy.

beforeEach ->
  scope.users = ["Dave", "Steve", "Bob", "Joe"]
  inject ($q) ->
    deferred = $q.defer()
    spyOn(userService, "delete").and.returnValue(deferred.promise)

  ctrl.deleteUser("Bob")

it "should delete the user", ->
  expect(userService.delete).toHaveBeenCalledWith("Bob")

This takes care of the first part of the test - checking that the userService is actually called with the specified user. Now we need to test the callback code - what happens when the promise is resolved.

Test the asynchronous code

In order to test the callback code we simply need to store a reference to the deferred object, and then call resolve on this object. Once we resolve the promise we also need to call $apply on the scope to force angular to run a digest cycle.

describe "when the user is successfully deleted", ->
  beforeEach ->
    deferred.resolve()
    scope.$apply()

  it "removes the user from the list of users", ->
    expect(scope.users).toEqual(["Dave", "Steve", "Joe"])

That’s all there is to it. I find it really easy to test asynchronous code in angular - which really means no code needs to be untested.

You can find the entire example on Plunker. Happy coding.