Best practices for writing testable AngularJS code

Loading

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');
});
});

Leave a Reply

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