Testing Angular Services with Dependencies

2015-07-28

One of the nicest features of AngularJS is its inherent testability.

Testing functions with dependencies is easy. One simply passes in mock objects to the function in question in a test. Likewise, testing controllers with dependencies is also fairly straightforward. One simply instantiates a controller, giving it whatever mocked dependencies are necessary, and then proceeds with the test. If we skip the details of the setup (see here for those details), newing up the controller looks something like the following:

MyCoolCtrl = $controller("MyCoolCtrl", {
  MyCoolService: MyMockedService
});

We can mock the service object in whatever way we like and then hand that mocked object over to the controller. Then after invoking some number of controller functions, we can easily verify the mock is used in some way or another.

What then does a similar test of a service with dependencies look like?

Say, for instance, we want to write a simple service to wrap localStorage. We know that we cannot pass in the real window object in the test as it does not exist in that context. Hence, $window -- Angular's convenient wrapper around the window object. But how do we pass $window to our service? Unlike the $controller method above, we do not have a similar method for instantiating service objects without short-circuiting Angular's dependency injection.

Enter $provide. We can swap out $window for a mock window object whose interface matches the real $window, at least insofar as our object under test in concerned.

Let's walk through a test for our envisioned LocalStorageService. First, we can assure that when an object asks for $window, it will instead get a mock window, which we control.

describe("LocalStorageService", function () {
  // ... mockWindow definition goes here

  beforeEach(module("myApp", function ($provide) {
    $provide.value("$window", mockWindow);
  }));

  // ... tests go here

In the lines above, we are configuring the $window provider and telling it to instead hand over our mockWindow object. What does mockWindow look like?

Since we know the interface of localStorage (see here), we could could create a mockWindow which would look something like the following:

describe("LocalStorageService", function () {
  var LocalStorageService,  // a local variable for our object under test
    mockWindow = {          // <---------- This is our fake window object
      localStorage: {
        _storage: {},
        getItem: function (k) {
          return this._storage[k];
        },
        setItem: function (k, v) {
          this._storage[k] = v;
        },
        removeItem: function (k) {
          delete this._storage[k];
        }
      }
    };

  // ... provider config from above

  // ... our tests will go here
});

Even though JavaScript does not make it explicit, our mockWindow object has a localStorage which, implementation aside, has all the relevant methods we require of the true localStorage object. In effect, we have built a fake window object for use in test that looks exactly like the real $window object.

From there, it's trivial to write our tests:

describe("LocalStorageService", function () {
  // ... LocalStorageService and mockWindow declaration

  // ... configuration of the $window provider

  // we inject our service and store it off
  // in the local variable declared above
  beforeEach(inject(function (_LocalStorageService_) {
    LocalStorageService = _LocalStorageService_;
  }));

  it("returns a value for a key", function () {
    mockWindow.localStorage.setItem("a key", "a value");

    expect(LocalStorageService.getKey("a key")).toEqual("a value");
  });

  it("stores a value for a key", function () {
    LocalStorageService.setKey("my key", "my value");

    expect(mockWindow.localStorage.getItem("my key")).toEqual("my value");
  });

  it("removes a value for a given key", function () {
    mockWindow.localStorage.setItem("bad key", "bad value");

    LocalStorageService.remove("bad key");

    expect(mockWindow.localStorage.getItem("bad key")).toEqual(undefined);
  });
});

And with that, we have a clean test with our service's dependency on $window replaced by a mock object which we can easily verify.

For those curious, the implementation of LocalStorageService might look like the following:

(function () {
  "use strict";

  function LocalStorageService($window) {
    function getKey(k) {
      return $window.localStorage.getItem(k);
    }

    function setKey(k, v) {
      $window.localStorage.setItem(k, v);
    }

    function remove(k) {
      $window.localStorage.removeItem(k);
    }

    // we return the public interface of our object
    return {
      getKey: getKey,
      setKey: setKey,
      remove: remove
    };
  }

  angular
    .module("myApp")
    .service("LocalStorageService", LocalStorageService);
})();