Mocking AngularJS services in unit tests not working

Loading

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() and inject().

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.

Leave a Reply

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