3

I have a function in a controller that calls a service which in turn calls another service, which makes the AJAX call and returns the promise. To put it simply:

MainController
    call = function(){
        //Handle the promise here, update some configs and stuff
        someService.call().then(success, failure);
    }

SomeService
    return {
        call: function(){
            return SomeOtherService.call();
        }
    }

SomeOtherService
    return {
        call: function(){
            return $http.get(someUrl);
        }          
    }

How do I test the main controller's call function without actually making the AJAX call? I will obviously be testing the services separately.

Update: So I made the changes as per the answer below, and this is what I get:

angular.module('myApp')
.controller('LoginCtrl', ['$scope', '$location', 'AuthService', '$state', function ($scope, $location, AuthService, $state) {
    $scope.user     = {};
    $scope.login    = function(user) {
        $scope.user = user;

        //This login function in turn calls the 'authenticate' from another service 'Ajax' and the promise is handled here.
        AuthService.login($scope.user)
            .then(function() {
                $scope.loginStatus = {
                    message     : 'Login successful',
                    alertClass  : 'success'
                };
                $state.go('app.dashboard');
            }, function() {
                $scope.loginStatus = {
                    message     : 'Unauthorized access.',
                    alertClass  : 'danger'
                };
            });
        };

    $scope.closeAlert = function() {
        $scope.loginStatus = false;
    };
}]);

PhantomJS 1.9.7 (Linux) Controller: LoginCtrl should call login FAILED
TypeError: 'undefined' is not an object (evaluating 'AuthService.login($scope.user)
                .then')
    at /home/rutwickg/Projects/workspace/myProject/app/scripts/controllers/login.js:15
    at /home/rutwickg/Projects/workspace/myProject/test/spec/controllers/login.js:34
    at /home/rutwickg/Projects/workspace/myProject/node_modules/karma-jasmine/lib/boot.js:117
    at /home/rutwickg/Projects/workspace/myProject/node_modules/karma-jasmine/lib/adapter.js:171
    at http://localhost:8080/karma.js:189
    at http://localhost:8080/context.html:145

This is how my current code looks like:

'use strict';

describe('Controller: LoginCtrl', function() {

// load the controller's module
beforeEach(module('sseFeApp'));

var LoginCtrl,
    scope,
    authMock,
    ajaxMock,
    q;

// Initialize the controller and a mock scope
beforeEach( inject(function($controller, $rootScope, _AuthService_, _Ajax_, $q) {
        authMock = _AuthService_;
        ajaxMock = _Ajax_;
        q = $q;

        scope = $rootScope.$new();
        LoginCtrl = $controller('LoginCtrl', {
            $scope: scope
        });
    }));

it('should call login', function() {
        var deferred = q.defer();
        spyOn( authMock, 'login').and.returnValue(deferred.promise);
        $scope.login({username: 'Sample', password: 'Sample'});
        deferred.reject({});
        expect( authMock.login ).toHaveBeenCalled();

        //expect( ajaxMock.login).toHaveBeenCalled(); //Expect the second service call
    });
});

Update: According to the new syntax, it should and.returnValue(deferred.promise). Works!

Rutwick Gangurde
  • 4,772
  • 11
  • 53
  • 87

1 Answers1

1

You just inject your own Service and Spy on the function you're calling.

describe( 'Controller: myCtrl', function () {

  var myService,
      ...;

  beforeEach( inject( function ( _myService_ ) {
    ...
    myService = _myService_;
    ...
  } ) );

  it( 'should call myService.call', inject(function ($q)
  {
    var deferred = $q.defer();
    spyOn( myService, 'call' ).andReturn(deferred.promise);
    // .and.returnValue() for jasmine 2.0
    $scope.myControllerFn();
    deferred.resolve( {} ); // for success 
    // deferred.reject() --> for error
    $scope.digest(); // do this if you want success/error to be executed.
    expect( myService.call ).toHaveBeenCalled();
  } );
} );
m.brand
  • 658
  • 5
  • 12
  • Please let me try this. – Rutwick Gangurde Oct 21 '14 at 14:23
  • This gives me 'undefined as an object' error. Also, this code tests only one level of service. My controller calls a service which in turn calls another service. – Rutwick Gangurde Oct 22 '14 at 05:27
  • Where does the error happen? What is actually undefined? The second "level" pretty much works the same. You inject the service you want to mock and spyOn the function you expect to be called. – m.brand Oct 22 '14 at 06:10
  • I have updated my question description. Please check. – Rutwick Gangurde Oct 22 '14 at 08:51
  • Now it says `$q.defer()` is undefined! – Rutwick Gangurde Oct 22 '14 at 13:52
  • that's probably because you didn't inject it. – m.brand Oct 23 '14 at 07:58
  • Oh wait, I think it has got to do with the syntax. It should be `spyOn(...).and.returnValue();`. – Rutwick Gangurde Oct 27 '14 at 08:15
  • Thanks a lot @m.brand, it works! Just a single doubt. How do I check what the `loginStatus` variable contains depending on whether the promise was rejected or resolved? Should I ask a new question for it? – Rutwick Gangurde Oct 27 '14 at 08:29
  • 1
    Well, if you do a $scope.$digest() after the resolve or the reject the success or error is beeing called. So if you want to test your success part you have to resolve() the deferred and if you want to test your error part you have to reject() your deferred. – m.brand Oct 28 '14 at 11:03
  • This solves an issue I might have had later on, but the current issue I'm having is that I need to test a function that calls a function that fires an AJAX request. This doesn't quite do it for my situation. If you have any suggestions, I have a question open at http://stackoverflow.com/questions/31975928/how-do-i-test-a-function-that-calls-an-ajax-function-without-firing-it-using-jas. Thanks, C§ – CSS Aug 12 '15 at 21:53