Mocking Services That Return Promises in AngularJS Jasmine Unit Tests: Best Practices

Dec 05, 2025 · Programming · 16 views · 7.8

Keywords: AngularJS | Unit Testing | Jasmine | Promise Mocking | spyOn

Abstract: This article explores how to properly mock services that return promises in AngularJS unit tests using Jasmine. It analyzes common error patterns, explains two methods using $provide.value and spyOn with detailed code examples, and discusses the necessity of $digest calls. Tips for avoiding reference update issues are provided to ensure test reliability and maintainability.

In unit testing AngularJS applications, mocking dependent services is a common yet error-prone task, especially when these services return promises. The asynchronous nature of promises complicates test logic, requiring careful attention to mock implementation and test execution flow. Based on a typical scenario, this article explains in detail how to correctly mock services that return promises and offers best practice recommendations.

Problem Scenario Analysis

Suppose we have a service myService that depends on another service myOtherService. The latter provides a method makeRemoteCallReturningPromise that performs a remote call and returns a promise. When testing myService, we need to mock myOtherService to avoid actual network requests while controlling promise resolution behavior. In initial attempts, developers might use $provide.value to inject a mock object but encounter reference update issues, causing the mock to not take effect.

Error Pattern Analysis

In the initial code, the developer tries to inject an empty object as a mock using $provide.value in a beforeEach block, then redefines the object's method in another beforeEach. However, due to JavaScript's object reference mechanism, reassigning myOtherServiceMock does not update the reference previously injected via $provide.value. This results in the test using an empty object, causing errors like makeRemoteCall being undefined. For example, the code attempts to set myOtherServiceMock to a new object, but $provide cannot detect this change.

Solution 1: Using the spyOn Method

The best practice is to use Jasmine's spyOn function to mock service methods. This approach is more concise and avoids reference issues. First, load the module with beforeEach(module('app.myService')), then inject dependencies using inject. In the injection block, call spyOn(myOtherService, "makeRemoteCallReturningPromise").and.callFake(function() { ... }) to mock the method. Internally, use $q.defer() to create a promise and set the resolution value with deferred.resolve('Remote call result'). This ensures the mock method is correctly invoked during testing.

describe('Testing remote call returning promise', function() {
  var myService;

  beforeEach(module('app.myService'));

  beforeEach(inject(function(_myService_, myOtherService, $q) {
    myService = _myService_;
    spyOn(myOtherService, "makeRemoteCallReturningPromise").and.callFake(function() {
      var deferred = $q.defer();
      deferred.resolve('Remote call result');
      return deferred.promise;
    });
  }));

  it('can do remote call', inject(function() {
    myService.makeRemoteCall()
      .then(function(result) {
        expect(result).toBe('Remote call result');
      });
  }));
});

Solution 2: Updating Existing References

If persisting with $provide.value, ensure not to reassign the mock object but update its properties. In the injection block, directly assign a new function to myOtherServiceMock.makeRemoteCallReturningPromise, so the reference injected by $provide remains unchanged but the method behavior is updated. For example: myOtherServiceMock.makeRemoteCallReturningPromise = function() { ... }. This method works but is less intuitive than spyOn and prone to errors.

Key Points in Promise Testing

In AngularJS, promise resolution requires triggering a $digest cycle for then callbacks to execute. In tests, it is often necessary to manually call $rootScope.$digest() or use $rootScope.$apply() after invoking the promise. For example, inject $rootScope in the test block and add $rootScope.$digest() after the promise call. This synchronizes asynchronous operations, allowing assertions to correctly verify results.

Alternative Method: Using $q.when

As an alternative, use $q.when to directly create a resolved promise, simplifying mock code. For example: spyOn(myOtherService, "makeRemoteCallReturningPromise").and.returnValue($q.when('Remote call result')). This method is suitable for Jasmine 2 and is more concise, but may not apply to scenarios requiring complex control.

Summary and Best Practices

When mocking services that return promises, it is recommended to use spyOn with and.callFake for better control and readability. Always remember to trigger the $digest cycle in tests to ensure promise callbacks execute. Avoid reassigning mock object references; instead, update their properties or use spies. By following these practices, unit test reliability and maintainability can be enhanced, effectively validating AngularJS service behavior.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.