Cannot spy on AngularJS service methods using Jasmine

Loading

When you encounter the issue “Cannot spy on AngularJS service methods using Jasmine,” it usually means you’re trying to spy on a method that either:

  • Does not exist at the time you’re creating the spy,
  • Is not properly injected/mocked in the test,
  • Or you’re spying on an instance method rather than the prototype/reference being used by the controller or component under test.

Let’s walk through step-by-step (detailed and around 1000 words) how to properly spy on AngularJS service methods using Jasmine.


Understanding the Context

AngularJS uses dependency injection, and most services are singleton objects. In Jasmine, spying means wrapping a function to monitor calls, arguments, and outcomes using spyOn().

spyOn(myService, 'someMethod').and.returnValue(someValue);

This only works if myService and someMethod already exist at the time the spy is created.


Common Errors

  1. “spyOn could not find an object to spy upon for myService.someMethod”
  2. “TypeError: Cannot read property ‘someMethod’ of undefined”
  3. Service not being injected correctly
  4. Spying on a method after it was already called in the beforeEach

Step-by-Step Guide to Spy on AngularJS Services


1. Define the Service

Let’s say we have this simple AngularJS service:

app.service('UserService', function($http) {
  this.getUser = function(id) {
    return $http.get('/api/users/' + id);
  };
});

2. Controller Using the Service

app.controller('UserCtrl', function($scope, UserService) {
  UserService.getUser(1).then(function(response) {
    $scope.user = response.data;
  });
});

3. Set Up the Jasmine Test Suite

describe('UserCtrl', function() {
  var $controller, $rootScope, $q, $scope, UserService;

  // Load the module
  beforeEach(module('myApp'));

  // Inject dependencies
  beforeEach(inject(function(_$controller_, _$rootScope_, _$q_, _UserService_) {
    $controller = _$controller_;
    $rootScope = _$rootScope_;
    $q = _$q_;
    $scope = $rootScope.$new();
    UserService = _UserService_;
  }));

4. Spy on the Service Method (Before Controller Instantiation)

This is key: you must spy on the method before the controller is created, otherwise the method will be called before Jasmine can track it.

  it('should call UserService.getUser', function() {
    spyOn(UserService, 'getUser').and.callFake(function() {
      var deferred = $q.defer();
      deferred.resolve({ data: { name: 'Test User' } });
      return deferred.promise;
    });

    $controller('UserCtrl', {
      $scope: $scope,
      UserService: UserService
    });

    // Resolve the promise
    $rootScope.$apply();

    expect(UserService.getUser).toHaveBeenCalledWith(1);
    expect($scope.user.name).toBe('Test User');
  });
});

Fixing the “Cannot Spy On” Error

Ensure Proper Injection

Make sure you’re injecting the actual AngularJS service using the underscore syntax convention:

inject(function(_MyService_) {
  MyService = _MyService_;
});

This ensures you’re spying on the real service, not an undefined or stub object.


Use a Mock Instead (if needed)

Sometimes, it’s cleaner to mock the service entirely:

beforeEach(module(function($provide) {
  mockUserService = {
    getUser: jasmine.createSpy('getUser').and.callFake(function() {
      var deferred = $q.defer();
      deferred.resolve({ data: { name: 'Mocked User' } });
      return deferred.promise;
    })
  };
  $provide.value('UserService', mockUserService);
}));

Then proceed as normal:

$controller('UserCtrl', {
  $scope: $scope,
  UserService: mockUserService
});

Testing Edge Case – Method Doesn’t Exist Yet

If you try:

spyOn(UserService, 'nonExistingMethod');

It will throw:

Expected spy to be called, but it was never called
OR
Cannot spyOn on a non-function property

Fix:

  • Make sure the method exists in your service file.
  • Ensure that your spy is created before any call happens.
  • You can use jasmine.createSpy() or createSpyObj() for mocks.

Testing spyOn With Factories

If your service is defined as a factory:

app.factory('UserService', function() {
  return {
    getUser: function(id) { ... }
  };
});

Same rules apply:

beforeEach(inject(function(_UserService_) {
  UserService = _UserService_;
  spyOn(UserService, 'getUser').and.returnValue($q.resolve({ data: {} }));
}));

Cleanup Tips

  • Always call $rootScope.$apply() to resolve promises manually.
  • Use done() callback if working with async tests.
  • Avoid re-instantiating controller inside it blocks multiple times without cleaning $scope.

Summary Checklist

StepAction
1Ensure service is injected using inject()
2Confirm method exists before spyOn()
3Create spy before instantiating controller
4Use $q.defer() for promises and $rootScope.$apply() to resolve them
5Consider using $provide.value() for mocked services
6Use jasmine.createSpy() or createSpyObj() for isolated testing
7If still failing, check for race conditions or double instantiation

Leave a Reply

Your email address will not be published. Required fields are marked *