Testing controller that depends on a service which is based on resource

106 Views Asked by At

I have a controller that calls a Service which is a wrapper for a Resource. Like this:

app.factory("Service", ["$resource", function ($resource) {
            return $resource("/api/Service/get");
        }]);

Return value of the service's method is assigned to a variable within the controller. Normally, the variable is of type Resource and it contains a promise. When the promise is resolved, the variable is populated with all values returned from the backend. I track then on the promise in order to modify the model received from the backend. Like so:

this.model = Service.get();
    this.model.$promise.then(function(data) {
        // do something with data
    });

I need to test the value of the resulting model variable in my controller. The only way I found to do this, is to use $httpBackend with a real implementation of my Service. However, this is ugly because then, testing my controller, I have to pass request path "api/Service/get" to the httpBackend.when() in order for it to respond with some value.

An excerpt form my test:

// call Controller
$httpBackend.when('GET', '/api/Service/get').respond(someData);
$httpBackend.flush();
expect(scope.model.property).toBe(null);

This seems and feels utterly wrong. The whole point of using a separate service to deal with resource is for the controller to not know anything about the url and http method name. So what should I do?

In other words, what I want to test is that then gets called and does what I need it to do.

I guess I could probably create a separate service that gets called in then and do what I need to do with the model but it feels a bit overkill if all I want to do is, for example, set one field to null depending on a simple condition.

1

There are 1 best solutions below

0
On

You are correct, you shouldn't have to use $httpBackend unless you are using $http in the controller you are testing.

As you wrote, the controller shouldn't need to know anything about the implementation of Service. What the controller knows is that Service has a get method that returns an object with a $promise property that is a promise.

What you want to do is to use a fake implementation of Service in your test. There are multiple ways to do this via mocks, spies, stubs etc, depending on your use case and which testing framework(s) you are using.

One way is to create a fake implementation like this:

var Service = {
  get: function() {

    deferred = $q.defer();

    return {
      $promise: deferred.promise
    };
  }
};

You want to be able to access deferred from the tests, so you can either resolve or reject the promise based on what you want to test.

Full setup:

var $rootScope,
  scope,
  createController,
  $q,
  deferred;

var Service = {
  get: function() {

    deferred = $q.defer();

    return {
      $promise: deferred.promise
    };
  }
};

beforeEach(function() {

  module('App');

  inject(function(_$rootScope_, $controller, _$q_) {

    $rootScope = _$rootScope_;

    scope = $rootScope.$new();

    createController = function() {
      $controller('MyController', {
        $scope: scope,
        Service: Service
      });
    };

    $q = _$q_;
  });
});

Controller implementation:

app.controller('MyController', function($scope, Service) {

  $scope.property = false;

  $scope.model = Service.get();

  $scope.model.$promise.then(function(data) {

    if (data) {
      $scope.property = true;
    }
  });
});

You can then spy on the fake implementation to assert that it is called correctly.

Example with Jasmine:

spyOn(Service, 'get').and.callThrough();

You need and.callThrough() or the call will be interrupted and your fake implementation will not be used.

You now have full control by manually creating the controller, resolving the promise and triggering the digest loop and can test the different states:

it('Should work', function() {

  spyOn(Service, 'get').and.callThrough();

  expect(Service.get).not.toHaveBeenCalled();

  createController();

  expect(Service.get).toHaveBeenCalled();

  expect(scope.property).toBeFalsy();

  deferred.resolve('some data');
  $rootScope.$digest();

  expect(scope.property).toBeTruthy();
});

Demo: http://plnkr.co/edit/th2pLWdVa8AZWOyecWOF?p=preview