Matt MacLeod
Matt MacLeod

Reputation: 448

Spy on scope function that executes when an angular controller is initialized

I want to test that the following function is in fact called upon the initialization of this controller using jasmine. It seems like using a spy is the way to go, It just isn't working as I'd expect when I put the expectation for it to have been called in an 'it' block. I'm wondering if there is a special way to check if something was called when it wasn't called within a scope function, but just in the controller itself.

 App.controller('aCtrl', [ '$scope', function($scope){

    $scope.loadResponses = function(){
        //do something
    }

    $scope.loadResponses();

}]);

//spec file

describe('test spec', function(){

    beforeEach(
    //rootscope assigned to scope, scope injected into controller, controller instantiation.. the expected stuff

        spyOn(scope, 'loadResponses');
    );

    it('should ensure that scope.loadResponses was called upon instantiation of the controller', function(){
         expect(scope.loadResponses).toHaveBeenCalled();
    });
});

Upvotes: 18

Views: 17075

Answers (4)

Matt MacLeod
Matt MacLeod

Reputation: 448

Setting the spy before controller instantiation (in the beforeEach) is the way to test controller functions that execute upon instantiation.

EDIT: There is more to it. As a comment points out, the function doesn't exist at the time of ctrl instantiation. To spy on that call you need to assign an arbitrary function to the variable (in this case you assign scope.getResponses to an empty function) in your setup block AFTER you have scope, but BEFORE you instantiate the controller. Then you need to write the spy (again in your setup block and BEFORE ctrl instantiation), and finally you can instantiate the controller and expect a call to have been made to that function. Sorry for the crappy answer initially

Upvotes: 4

Konamiman
Konamiman

Reputation: 50323

The only way I have found to test this type of scenarios is moving the method to be tested to a separate dependency, then inject it in the controller, and provide a fake in the tests instead.

Here is a very basic working example:

angular.module('test', [])
    .factory('loadResponses', function() {
        return function() {
            //do something
        }
    })
    .controller('aCtrl', ['$scope', 'loadResponses', function($scope, loadResponses) {
        $scope.loadResponses = loadResponses;

        $scope.loadResponses();
    }]);

describe('test spec', function(){
    var scope;
    var loadResponsesInvoked = false;

    var fakeLoadResponses = function () {
        loadResponsesInvoked = true;
    }

    beforeEach(function () {
        module('test', function($provide) {
            $provide.value('loadResponses', fakeLoadResponses)
        });

        inject(function($controller, $rootScope) {
            scope = $rootScope.$new();
            $controller('aCtrl', { $scope: scope });
        });
    });

    it('should ensure that scope.loadResponses was called upon instantiation of the controller', function () {
        expect(loadResponsesInvoked).toBeTruthy();
    });
});

For real world code you will probably need extra work (for example, you may not always want to fake the loadResponses method), but you get the idea.

Also, here is a nice article that explains how to create fake dependencies that actually use Jasmine spies: Mocking Dependencies in AngularJS Tests

EDIT: Here is an alternative way, that uses $provide.delegate and does not replace the original method:

describe('test spec', function(){
    var scope, loadResponses;
    var loadResponsesInvoked = false;

    beforeEach(function () {
        var loadResponsesDecorator = function ($delegate) {
            loadResponsesInvoked = true;
            return $delegate;
        }

        module('test', function($provide) {
            $provide.decorator('loadResponses', loadResponsesDecorator);
        });

        inject(function($controller, $rootScope) {
            scope = $rootScope.$new();
            $controller('aCtrl', { $scope: scope });
        });
    });

    it('should ensure that scope.loadResponses was called upon instantiation of the controller', function () {
        expect(loadResponsesInvoked).toBeTruthy();
    });
});

Upvotes: 2

guy mograbi
guy mograbi

Reputation: 28728

I didn't quite understand any of the answers above.

the method I often use - don't test it, instead test the output it makes..

you have not specified what loadResponses actually does.. but lets say it puts something on scope - so test existence of that..

BTW - I myself asked a similar question but on an isolated scope angular - how to test directive with isolatedScope load?

if you still want to spy - on an unisolated scope, you could definitely use a technique..

for example, change your code to be

 if ( !$scope.loadResponses ){
     $scope.loadResponses = function(){}
 } 

 $scope.loadResponses();

This way you will be able to define the spy before initializing the controller.

Another way, is like PSL suggested in the comments - move loadResponses to a service, spy on that and check it has been called.

However, as mentioned, this won't work on an isolated scope.. and so the method of testing the output of it is the only one I really recommend as it answers both scenarios.

Upvotes: 0

Zenorbi
Zenorbi

Reputation: 2644

You need to initialise the controller yourself with the scope you've created. The problem is, that you need to restructure your code. You can't spy on a non-existing function, but you need to spyOn before the function gets called.

$scope.loadResponses = function(){
    //do something
}
// <-- You would need your spy attached here
$scope.loadResponses();

Since you cannot do that, you need to make the $scope.loadResponses() call elsewhere.

The code that would successfully spy on a scoped function is this:

var scope;
beforeEach(inject(function($controller, $rootScope) {
    scope = $rootScope.$new();
    $controller('aCtrl', {$scope: scope});
    scope.$digest();
}));
it("should have been called", function() {
    spyOn(scope, "loadResponses");
    scope.doTheStuffThatMakedLoadResponsesCalled();
    expect(scope.loadResponses).toHaveBeenCalled();
});

Upvotes: 9

Related Questions