Writing testable code in AngularJS ensures maintainability, scalability, and reliability. Following best practices allows for efficient unit testing, end-to-end testing, and debugging.
1. Follow the Single Responsibility Principle (SRP)
Each component should have one responsibility to make it easier to test.
Example: Well-structured Service
app.service('AuthService', function($http) {
this.login = function(credentials) {
return $http.post('/api/login', credentials);
};
});
Example: Service Doing Too Much
app.service('AuthService', function($http) {
this.login = function(credentials) {
return $http.post('/api/login', credentials);
};
this.getProfile = function() {
return $http.get('/api/user-profile');
};
});
Fix: Split into AuthService
and UserProfileService
.
2. Use Dependency Injection (DI) Properly
Avoid hardcoded dependencies to enable mocking in tests.
Example: Injectable Service
app.service('UserService', function($http) {
this.getUser = function() {
return $http.get('/api/user');
};
});
Example: Injecting in Controller
app.controller('MainCtrl', function(UserService) {
UserService.getUser().then(function(user) {
console.log(user);
});
});
Why?
✔ Easier to inject mocks during unit testing.
✔ No need to modify code for testing.
3. Use $httpBackend
to Mock API Calls
Avoid making real API requests in unit tests.
Example: Mocking $http
in Tests
describe('UserService', function() {
var UserService, $httpBackend;
beforeEach(module('app'));
beforeEach(inject(function(_UserService_, _$httpBackend_) {
UserService = _UserService_;
$httpBackend = _$httpBackend_;
}));
it('should fetch user data', function() {
$httpBackend.expectGET('/api/user').respond(200, { name: 'John' });
UserService.getUser().then(function(response) {
expect(response.data.name).toBe('John');
});
$httpBackend.flush();
});
});
4. Use $q
for Handling Promises in Services
Example: Returning a Promise
app.service('DataService', function($http, $q) {
this.getData = function() {
var deferred = $q.defer();
$http.get('/api/data')
.then(function(response) {
deferred.resolve(response.data);
})
.catch(function(error) {
deferred.reject(error);
});
return deferred.promise;
};
});
Why?
✔ Easier to test with $q.defer()
.
✔ Avoids deep callback nesting.
5. Write Unit Tests for Controllers
Avoid using $scope
directly in controllers to improve testability.
Example: Controller with Dependency Injection
app.controller('MainCtrl', function(UserService) {
var vm = this;
vm.user = {};
UserService.getUser().then(function(data) {
vm.user = data;
});
});
Unit Test for Controller
describe('MainCtrl', function() {
var $controller, UserService, $q, $rootScope;
beforeEach(module('app'));
beforeEach(inject(function(_$controller_, _UserService_, _$q_, _$rootScope_) {
$controller = _$controller_;
UserService = _UserService_;
$q = _$q_;
$rootScope = _$rootScope_;
}));
it('should load user data', function() {
spyOn(UserService, 'getUser').and.returnValue($q.resolve({ name: 'Alice' }));
var ctrl = $controller('MainCtrl');
$rootScope.$apply();
expect(ctrl.user.name).toBe('Alice');
});
});
6. Use $watch
and $digest
Sparingly
Using too many $watch
statements affects performance and makes testing harder.
Avoid Excessive $watch
$scope.$watch('inputValue', function(newVal, oldVal) {
if (newVal !== oldVal) {
console.log('Value changed');
}
});
Use $watchCollection
or One-Time Binding
$scope.$watchCollection('items', function(newItems) {
console.log('Items updated:', newItems);
});
7. Use Factories Instead of Services for Stateful Objects
Factories allow more flexible singleton services.
Example: Factory for Data Sharing
app.factory('CartService', function() {
var cart = [];
return {
addItem: function(item) {
cart.push(item);
},
getItems: function() {
return cart;
}
};
});
Unit Test for Factory
describe('CartService', function() {
var CartService;
beforeEach(module('app'));
beforeEach(inject(function(_CartService_) {
CartService = _CartService_;
}));
it('should add items to the cart', function() {
CartService.addItem({ name: 'Book' });
expect(CartService.getItems().length).toBe(1);
});
});
8. Use ngMock
for Dependency Injection in Tests
ngMock
provides utilities for mocking dependencies in unit tests.
Example: Mocking a Service
describe('AuthService', function() {
var AuthService, $httpBackend;
beforeEach(module('app'));
beforeEach(inject(function(_AuthService_, _$httpBackend_) {
AuthService = _AuthService_;
$httpBackend = _$httpBackend_;
}));
it('should send login request', function() {
$httpBackend.expectPOST('/api/login').respond(200, { token: 'abc123' });
AuthService.login({ username: 'admin', password: 'pass' })
.then(function(response) {
expect(response.data.token).toBe('abc123');
});
$httpBackend.flush();
});
});
9. Avoid Direct DOM Manipulation in Controllers
Use directives for DOM changes instead.
Bad Practice
$scope.changeColor = function() {
document.getElementById('myDiv').style.backgroundColor = 'red';
};
Good Practice: Using a Directive
app.directive('highlight', function() {
return {
restrict: 'A',
link: function(scope, element) {
element.css('background-color', 'red');
}
};
});
10. Write End-to-End (E2E) Tests Using Protractor
describe('Login Page', function() {
it('should log in successfully', function() {
browser.get('/login');
element(by.model('user.username')).sendKeys('admin');
element(by.model('user.password')).sendKeys('password');
element(by.buttonText('Login')).click();
expect(browser.getCurrentUrl()).toContain('/dashboard');
});
});