Testing AngularJS controllers ensures that the logic inside them works as expected. We typically use Karma as the test runner and Jasmine as the testing framework.
This guide covers:
- Setting Up Karma and Jasmine
- Testing a Simple Controller
- Testing Controllers with Dependencies
- Mocking Services in Controller Tests
- Best Practices for Controller Testing
1. Setting Up Karma and Jasmine
Before writing tests, ensure you have installed Karma and Jasmine.
Step 1: Install Karma and Jasmine
npm install -g karma karma-cli jasmine-core karma-jasmine karma-chrome-launcher
Step 2: Initialize Karma
karma init karma.conf.js
Configure the settings to use:
- Test Framework: Jasmine
- Browser: Chrome
- Pattern for Test Files:
test/**/*.spec.js
2. Testing a Simple Controller
Let’s start with a basic AngularJS controller.
Controller Code (app.js
)
var app = angular.module('myApp', []);
app.controller('MainController', function($scope) {
$scope.message = "Hello, World!";
});
Test Case for Controller (mainController.spec.js
)
describe('MainController', function() {
var $controller, $rootScope;
beforeEach(module('myApp'));
beforeEach(inject(function(_$controller_, _$rootScope_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
}));
it('should have a message defined', function() {
var $scope = $rootScope.$new();
var controller = $controller('MainController', { $scope: $scope });
expect($scope.message).toBe("Hello, World!");
});
});
Explanation:
beforeEach(module('myApp'))
→ Loads the AngularJS module.beforeEach(inject(...))
→ Injects dependencies for testing.$controller
→ Instantiates the controller.$rootScope.$new()
→ Creates a new scope for the test.
3. Testing Controllers with Dependencies
Many controllers rely on services or factories. Let’s test a controller that depends on a service.
Service Code (mathService.js
)
app.service('MathService', function() {
this.add = function(a, b) {
return a + b;
};
});
Controller Code (calculatorController.js
)
jsCopyEditapp.controller('CalculatorController', function($scope, MathService) {
$scope.result = 0;
$scope.addNumbers = function(a, b) {
$scope.result = MathService.add(a, b);
};
});
Test Case for Controller (calculatorController.spec.js
)
describe('CalculatorController', function() {
var $controller, $rootScope, MathService;
beforeEach(module('myApp'));
beforeEach(inject(function(_$controller_, _$rootScope_, _MathService_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
MathService = _MathService_;
}));
it('should add numbers using MathService', function() {
var $scope = $rootScope.$new();
var controller = $controller('CalculatorController', { $scope: $scope });
spyOn(MathService, 'add').and.returnValue(10);
$scope.addNumbers(4, 6);
expect(MathService.add).toHaveBeenCalledWith(4, 6);
expect($scope.result).toBe(10);
});
});
Key Points:
spyOn(MathService, 'add').and.returnValue(10)
→ Mocks theadd()
method.- Ensures the service function is called with correct parameters.
- Verifies the controller updates
$scope.result
correctly.
4. Mocking Services in Controller Tests
Sometimes, a controller depends on an external service making API calls. We use $q
and $rootScope.$apply()
to test asynchronous operations.
Controller with $http
Service (userController.js
)
app.controller('UserController', function($scope, UserService) {
$scope.user = {};
$scope.fetchUser = function() {
UserService.getUser().then(function(response) {
$scope.user = response.data;
});
};
});
Mocking HTTP Calls in Test (userController.spec.js
)
describe('UserController', function() {
var $controller, $rootScope, UserService, $q, deferred;
beforeEach(module('myApp'));
beforeEach(inject(function(_$controller_, _$rootScope_, _UserService_, _$q_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
UserService = _UserService_;
$q = _$q_;
deferred = $q.defer();
spyOn(UserService, 'getUser').and.returnValue(deferred.promise);
}));
it('should fetch user data and update $scope.user', function() {
var $scope = $rootScope.$new();
var controller = $controller('UserController', { $scope: $scope });
$scope.fetchUser();
deferred.resolve({ data: { name: "John Doe" } });
$rootScope.$apply(); // Resolve the promise
expect($scope.user.name).toBe("John Doe");
});
});
Why Use $q.defer()
?
- Creates a fake promise that we control in tests.
deferred.resolve({ data: { name: "John Doe" } })
simulates API response.$rootScope.$apply()
forces AngularJS to process the promise.
5. Best Practices for Testing Controllers
✔ Keep controllers thin. Move business logic to services for easier testing.
✔ Use spies (spyOn
) to mock dependencies. Prevents real API calls.
✔ Test different scenarios. Handle valid inputs, errors, and edge cases.
✔ Mock $q
and $httpBackend
for async operations. Avoid real HTTP requests.
✔ Use $rootScope.$apply()
when testing promises. Ensures they resolve properly.
6. Running Tests
To execute the test cases, run:
karma start karma.conf.js
For continuous testing, enable auto-watch in karma.conf.js
:
autoWatch: true