How to test controllers in AngularJS?

Loading

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:

  1. Setting Up Karma and Jasmine
  2. Testing a Simple Controller
  3. Testing Controllers with Dependencies
  4. Mocking Services in Controller Tests
  5. 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 the add() 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

Leave a Reply

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