Mocking AngularJS Services in Unit Tests Not Working – Step-by-Step Troubleshooting Guide
Mocking services in AngularJS unit tests is essential for isolating your units of code (controllers, services, directives) and ensuring they behave as expected, without relying on external dependencies. However, developers sometimes run into issues where mocked services don’t behave correctly, or AngularJS continues to use the real implementation rather than the mock.
In this detailed guide, we’ll go step by step to understand how to correctly mock services in AngularJS unit tests using Jasmine, and how to debug issues when it doesn’t work as expected.
1. Understand the Problem
When mocking doesn’t work, you may face issues like:
- The test still calls the original (real) service instead of the mock.
- Injected mock returns
undefined
. - Angular throws an unknown provider error.
- Mock methods are not being called even though you expect them to.
2. Correct Pattern for Mocking Services in AngularJS
Here’s a working example pattern for mocking services:
Original Service:
angular.module('app').service('dataService', function() {
this.getData = function() {
return 'real data';
};
});
Controller using the service:
angular.module('app').controller('MainController', function(dataService) {
var vm = this;
vm.data = dataService.getData();
});
Correctly Mocked Test:
describe('MainController', function() {
var $controller, mockDataService;
beforeEach(module('app'));
beforeEach(module(function($provide) {
mockDataService = {
getData: jasmine.createSpy('getData').and.returnValue('mock data')
};
$provide.value('dataService', mockDataService);
}));
beforeEach(inject(function(_$controller_) {
$controller = _$controller_;
}));
it('should use mockDataService and call getData()', function() {
var controller = $controller('MainController');
expect(mockDataService.getData).toHaveBeenCalled();
expect(controller.data).toBe('mock data');
});
});
3. Checklist When Mocks Fail to Work
Use $provide.value()
Correctly
Make sure you use $provide.value()
before calling inject()
. $provide.value()
replaces the real service with a mock.
Use module()
Before inject()
You must set up the module and mock it before any inject()
calls.
Don’t Re-initialize the Real Module After Providing a Mock
If you write:
beforeEach(module('app'));
beforeEach(module(...)); // <-- redefining module after mocks can override them
Make sure you call module('app')
once before providing mocks, otherwise the mock gets overridden.
4. Common Issues and Fixes
Issue: Mock is ignored, real service used
Cause: Mock added after Angular already instantiated the original service.
Fix: Make sure $provide.value()
is inside beforeEach(module(...))
and is executed before inject()
.
Issue: undefined is not a function
Cause: Your mock method name may be mistyped, or your controller is trying to call a method that the mock doesn’t define.
Fix: Ensure your mock object defines the expected method:
mockDataService = {
getData: function() { return 'mock'; }
};
Issue: Unknown provider error
Cause: You used $provide.value('serviceName')
but forgot to load the module where it’s registered.
Fix: Ensure module('app')
is loaded and correctly spelled.
5. Mocking Services with Promises or $http
If your service returns a promise:
Service:
.service('dataService', function($q) {
this.getData = function() {
return $q.resolve('real');
};
});
Mocking:
beforeEach(module(function($provide) {
$provide.value('dataService', {
getData: function() {
var deferred = $q.defer();
deferred.resolve('mock');
return deferred.promise;
}
});
}));
Remember to use $rootScope.$apply()
or $scope.$digest()
in tests to resolve promises:
controller.dataService.getData().then(function(result) {
resultData = result;
});
$rootScope.$apply(); // resolves promise
6. Mocking Using Jasmine Spies
Instead of a full mock object, you can use Jasmine spies:
mockDataService = jasmine.createSpyObj('dataService', ['getData']);
mockDataService.getData.and.returnValue('mocked');
$provide.value('dataService', mockDataService);
This is helpful when you just want to assert that a method was called without mocking full behavior.
7. Mocking in Nested Modules
If your app has nested modules:
angular.module('feature', [])
.service('featureService', function() { ... });
angular.module('app', ['feature']);
Make sure to load both modules in the test if mocking a service from feature
.
beforeEach(module('app'));
beforeEach(module(function($provide) {
$provide.value('featureService', mockFeatureService);
}));
8. Debugging Tips
- Use
console.log()
inside mocks to ensure they’re being used. - Spy on real services temporarily to see if they’re being called.
- Use
angular.mock.dump()
to inspect scope values during tests. - Verify the order of
module()
andinject()
.
9. Alternative: $injector
to Manually Override Services
You can also override services directly via $injector
:
beforeEach(inject(function($injector) {
$injector.invoke(function($provide) {
$provide.value('dataService', mockDataService);
});
}));
But $provide
is more straightforward and preferred in most cases.