Asynchronous operations, such as HTTP requests, timers, and promises, are common in AngularJS applications. Testing these operations requires special handling to ensure test reliability.
In this guide, we will cover:
- Understanding Asynchronous Operations in AngularJS
- Using
$q
and Promises in Unit Tests - Mocking HTTP Calls with
$httpBackend
- Handling
$timeout
and$interval
in Tests - Using
$rootScope.$apply()
to Resolve Promises - Using
flush()
to Control Asynchronous Execution - Best Practices for Testing Asynchronous Code
1. Understanding Asynchronous Operations in AngularJS
Asynchronous operations in AngularJS include:
$http
for API requests.$q
for promises.$timeout
and$interval
for delayed execution.
Since these operations execute outside the normal test flow, unit tests must handle them explicitly.
2. Using $q
and Promises in Unit Tests
AngularJS uses the $q
service for promise-based asynchronous operations. We can mock promises in unit tests using $q.defer()
.
Example: Mocking a Service with $q
Consider this service that returns a promise:
app.service('DataService', function($q) {
this.getData = function() {
var deferred = $q.defer();
setTimeout(() => {
deferred.resolve('Success');
}, 1000);
return deferred.promise;
};
});
Unit Test for the Service
describe('DataService Test', function() {
var DataService, $rootScope;
beforeEach(module('myApp'));
beforeEach(inject(function(_DataService_, _$rootScope_) {
DataService = _DataService_;
$rootScope = _$rootScope_;
}));
it('should resolve the promise with "Success"', function() {
var result;
DataService.getData().then(function(response) {
result = response;
});
// Force promise resolution
$rootScope.$apply();
expect(result).toBe('Success');
});
});
Why $rootScope.$apply()
?
$rootScope.$apply()
forces the digest cycle, ensuring the promise is resolved immediately.
3. Mocking HTTP Calls with $httpBackend
The $httpBackend
service allows us to mock HTTP requests in unit tests.
Example: Service with $http
app.service('ApiService', function($http) {
this.getUsers = function() {
return $http.get('/api/users');
};
});
Unit Test with $httpBackend
describe('ApiService Test', function() {
var ApiService, $httpBackend;
beforeEach(module('myApp'));
beforeEach(inject(function(_ApiService_, _$httpBackend_) {
ApiService = _ApiService_;
$httpBackend = _$httpBackend_;
}));
it('should return user data from API', function() {
var mockResponse = [{ id: 1, name: 'John Doe' }];
// Mock the API call
$httpBackend.whenGET('/api/users').respond(200, mockResponse);
var result;
ApiService.getUsers().then(function(response) {
result = response.data;
});
// Flush pending requests
$httpBackend.flush();
expect(result).toEqual(mockResponse);
});
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
});
Key Takeaways
✔ $httpBackend.whenGET().respond()
mocks HTTP responses.
✔ $httpBackend.flush()
forces the response to be processed immediately.
✔ $httpBackend.verifyNoOutstandingRequest()
ensures all requests are handled.
4. Handling $timeout
and $interval
in Tests
AngularJS provides $timeout
and $interval
for delayed execution. We can use the flush()
method to control their execution in tests.
Example: Service with $timeout
app.service('TimerService', function($timeout) {
this.delayedMessage = function() {
var deferred = $timeout(function() {
return 'Hello after 3 seconds';
}, 3000);
return deferred;
};
});
Unit Test with $timeout.flush()
describe('TimerService Test', function() {
var TimerService, $timeout;
beforeEach(module('myApp'));
beforeEach(inject(function(_TimerService_, _$timeout_) {
TimerService = _TimerService_;
$timeout = _$timeout_;
}));
it('should return message after 3 seconds', function() {
var result;
TimerService.delayedMessage().then(function(response) {
result = response;
});
// Simulate 3 seconds passing
$timeout.flush(3000);
expect(result).toBe('Hello after 3 seconds');
});
});
Handling $interval
For $interval
, use $interval.flush()
.
$interval.flush(5000); // Simulates 5 seconds passing
5. Using $rootScope.$apply()
to Resolve Promises
AngularJS executes promises inside the digest cycle. When testing, we need to manually trigger the digest cycle using:
$rootScope.$apply();
This ensures that pending promises are resolved immediately.
6. Using flush()
to Control Asynchronous Execution
The flush()
method controls the execution of asynchronous functions:
$httpBackend.flush()
: Resolves pending HTTP requests.$timeout.flush(time)
: Triggers$timeout
after a given time.$interval.flush(time)
: Triggers$interval
after a given time.
Using flush()
ensures tests run deterministically without actual waiting.
7. Best Practices for Testing Asynchronous Code
✔ Use $httpBackend.flush()
to immediately process HTTP calls.
✔ Use $timeout.flush(time)
to control time-based execution.
✔ Use $rootScope.$apply()
to resolve promises.
✔ Ensure no outstanding requests remain ($httpBackend.verifyNoOutstandingRequest()
).
✔ Avoid actual delays in tests. Use mocks instead.