Writing unit tests for controllers using Jasmine

Loading

Unit testing is a crucial part of ensuring that your AngularJS application behaves as expected. Jasmine is a popular JavaScript testing framework that integrates well with AngularJS and provides a clean and readable syntax for writing tests. Unit testing AngularJS controllers using Jasmine allows you to validate their logic, behavior, and interactions with other components in isolation.

In this guide, we’ll walk through the steps of writing unit tests for AngularJS controllers using Jasmine. We’ll cover the basics, explore common testing patterns, and provide examples of testing various aspects of a controller, such as dependencies, functions, scope bindings, and interaction with services.


1. Setting Up the Testing Environment

Before writing tests, you need to set up your environment for testing. AngularJS provides the ngMock module, which simulates a browser environment for running tests, and it includes useful services like $rootScope, $controller, $injector, and $httpBackend.

First, make sure to include the necessary libraries:

  • Jasmine: The testing framework.
  • Karma: A test runner that integrates with Jasmine.
  • ngMock: Provides AngularJS-specific mocks, such as $controller.

Install Required Libraries:

To install the necessary dependencies using npm or bower, you can add them to your project.

Using npm:

npm install jasmine karma karma-jasmine karma-chrome-launcher --save-dev

Using bower:

bower install jasmine karma karma-jasmine --save-dev

2. Basic Structure of a Jasmine Unit Test for AngularJS Controller

A typical Jasmine test suite consists of the following components:

  • describe: Used to group tests together.
  • it: Defines an individual test case.
  • beforeEach: Runs before each test case to set up the initial state.
  • afterEach: Runs after each test case to clean up.

Here’s a basic example of a Jasmine test suite for an AngularJS controller.

describe('MyController', function() {
var $controller, $rootScope, controller;

// Load the module containing the controller
beforeEach(module('myApp'));

// Inject necessary services
beforeEach(inject(function(_$controller_, _$rootScope_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
}));

// Instantiate the controller before each test
beforeEach(function() {
controller = $controller('MyController', {
$scope: $rootScope.$new() // Create a new scope for the controller
});
});

// Example test case: Check if controller is defined
it('should be defined', function() {
expect(controller).toBeDefined();
});
});

3. Testing Controller Logic

Let’s assume you have a controller MyController that contains some logic, such as fetching data from a service and setting values on $scope. Here’s how you can write unit tests for such a controller.

Example Controller Code:

angular.module('myApp', [])
.controller('MyController', function($scope, MyService) {
$scope.greeting = "Hello, world!";

// Fetch data and set it on scope
MyService.getData().then(function(data) {
$scope.data = data;
});
})
.service('MyService', function($q) {
this.getData = function() {
var deferred = $q.defer();
deferred.resolve('Test Data');
return deferred.promise;
};
});

In this example, MyController sets a greeting and fetches data from MyService. You will need to test if the controller correctly interacts with the service and updates the scope.

Test Case for Controller Logic:

describe('MyController', function() {
var $controller, $rootScope, $q, MyService, controller;

beforeEach(module('myApp'));

beforeEach(inject(function(_$controller_, _$rootScope_, _$q_, _MyService_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
$q = _$q_;
MyService = _MyService_;
}));

beforeEach(function() {
// Create a new controller instance
controller = $controller('MyController', {
$scope: $rootScope.$new(),
MyService: MyService
});
});

it('should be defined', function() {
expect(controller).toBeDefined();
});

it('should set greeting on the scope', function() {
expect($rootScope.greeting).toBe('Hello, world!');
});

it('should fetch data and set it on the scope', function() {
spyOn(MyService, 'getData').and.callThrough();

// Simulate the promise resolution
MyService.getData().then(function(data) {
expect(data).toBe('Test Data');
expect($rootScope.data).toBe('Test Data');
});

// Trigger the digest cycle to resolve promises
$rootScope.$digest();
});
});

4. Testing Controller Interactions with Services

Controllers often depend on services to fetch data or perform business logic. To test controllers in isolation, you can mock or spy on the services the controller depends on.

For instance, if you have a service like MyService, you can spy on it and simulate its behavior using Jasmine’s spyOn function.

Mocking a Service:

it('should call getData on MyService', function() {
var mockService = jasmine.createSpyObj('MyService', ['getData']);
mockService.getData.and.returnValue($q.when('Mocked Data'));

controller = $controller('MyController', {
$scope: $rootScope.$new(),
MyService: mockService
});

controller.fetchData();

expect(mockService.getData).toHaveBeenCalled();
$rootScope.$digest(); // Trigger digest cycle for promise resolution
expect($rootScope.data).toBe('Mocked Data');
});

In this example:

  • We create a mock version of MyService using jasmine.createSpyObj.
  • The getData method is mocked to return a resolved promise with “Mocked Data”.
  • We check if the controller calls getData and if the data is set correctly on the $scope.

5. Testing Controller with HTTP Calls

Controllers often interact with APIs via $http. To mock HTTP requests in tests, you can use $httpBackend from ngMock.

Example Controller with $http:

angular.module('myApp', [])
.controller('HttpController', function($scope, $http) {
$http.get('/api/data').then(function(response) {
$scope.data = response.data;
});
});

Test Case for Controller with HTTP:

describe('HttpController', function() {
var $controller, $rootScope, $httpBackend, controller;

beforeEach(module('myApp'));

beforeEach(inject(function(_$controller_, _$rootScope_, _$httpBackend_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
$httpBackend = _$httpBackend_;
}));

beforeEach(function() {
controller = $controller('HttpController', {
$scope: $rootScope.$new()
});
});

it('should fetch data via $http and set it on scope', function() {
var mockData = { name: 'Test Data' };

// Mock the HTTP request
$httpBackend.whenGET('/api/data').respond(200, mockData);

// Trigger the HTTP request
$httpBackend.flush(); // Flush the HTTP request to resolve the promise

expect($rootScope.data).toEqual(mockData);
});
});

In this example:

  • We use $httpBackend to mock the HTTP request and simulate a response.
  • The controller’s $http.get is intercepted, and the response is provided.
  • We then verify that the data returned from the API is correctly set on the $scope.

Leave a Reply

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