Jasmine - Check if service method within a controller method was called

Problem

I started testing my ionic/angularjs app with Jasmine one day ago. I'm not sure if I totally misunderstood the idea of testing, but I want to test if a service method within a controller method was called and how the controller reacts to anything returned.

The controller function I'd like to test looks like this:

$scope.init = function() {
    DataService.fetchValues('dataprotection').then(function (result) {
        $scope.dataprotection = result;
    }, function (failure) {
        $scope.dataprotection = 'No dataprotection available';
    });
};

My Test should look like this:

describe('DataprotectionController', function () {
    beforeEach(inject(function ($rootScope, $controller, DataService) {
        scope = $rootScope.$new();
        controller = $controller('DataprotectionCtrl', {
            '$scope': scope
        });

        dataService = DataService;
    }));

    it('Should init dataprotection on startup', function () {
        // call init function in controller
        scope.init();
        //check if dataservice.fetchValues have been called with 'dataprotection' as parameter
        expect(dataService, 'fetchValues').toHaveBeenCalledWith('dataprotection');
        //calling fetchValues should init scope.dataprotection variable
        expect(scope.dataprotection).toBeDefined(); 
    });
});

Of course this is not working. Errorlogs tell me to create a spy object. So I did...

spyOn(dataService, 'fetchValues').andCallThrough();

Didn't help so I'm calling

dataService.fetchValues('dataprotection');

right after "scope.init();". First expect passes.

The thing I really don't understand is: why am I creating a spy object for the dataService fetchValues() method and then call it with a parameter and check if it was called with the given parameter? I don't want to call it manually, I want to check if dataService.fetchValues('dataprotection') was called inside the scope.init() function of DataprotectionController.

Sorry, if this is a really stupid question, but I'm literally stuck... Thanks for your help!

Problem courtesy of: error1337

Solution

The following syntaxes are for Jasmine 2.0, so if you are using Jasmine 1.3 you need to make some small changes.

First of all you need to inject the DataService into the controller:

var $scope,
    DataService,
    $q;

beforeEach(module('myApp'));

beforeEach(inject(function($controller, $rootScope, _DataService_, _$q_) {

  $scope = $rootScope.$new();
  DataService = _DataService_;

  controller = $controller('DataprotectionCtrl', {
    '$scope': $scope,
    'DataService': DataService
  });

  $q = _$q_;
}));

Note that if you use and.callThrough() the spy will delegate the function call to the real fetchValues implementation, unless you have replaced it with a mocked function yourself.

You can instead use and.callFake to return a promise:

spyOn(DataService, 'fetchValues').and.callFake(function(input) {
    var deferred = $q.defer();
    deferred.resolve('mock');
    return deferred.promise;
});

Otherwise the following code in your controller wouldn't return anything:

DataService.fetchValues('dataprotection')

Which means it would try to do the following on undefined:

.then(function(result) { ...

When using ngMock you need to synchronously maintain the flow of the tests, so after calling init you need to manually trigger a digest to get the promise to resolve:

$scope.init();

$scope.$digest();

And finally the syntax for verifying that the service function was called is:

expect(DataService.fetchValues).toHaveBeenCalledWith('dataprotection');

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

Solution courtesy of: tasseKATT

Discussion

There is currently no discussion for this recipe.

This recipe can be found in it's original form on Stack Over Flow.