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
- “spyOn could not find an object to spy upon for myService.someMethod”
- “TypeError: Cannot read property ‘someMethod’ of undefined”
- Service not being injected correctly
- 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
ORCannot 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()
orcreateSpyObj()
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
Step | Action |
---|---|
1 | Ensure service is injected using inject() |
2 | Confirm method exists before spyOn() |
3 | Create spy before instantiating controller |
4 | Use $q.defer() for promises and $rootScope.$apply() to resolve them |
5 | Consider using $provide.value() for mocked services |
6 | Use jasmine.createSpy() or createSpyObj() for isolated testing |
7 | If still failing, check for race conditions or double instantiation |