![]()
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
$qand Promises in Unit Tests - Mocking HTTP Calls with
$httpBackend - Handling
$timeoutand$intervalin 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:
$httpfor API requests.$qfor promises.$timeoutand$intervalfor 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$timeoutafter a given time.$interval.flush(time): Triggers$intervalafter 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.
