How to Unit Test an AngularJS Controller
A quick-start guide to writing unit tests for AngularJS controllers using Jasmine and ngMock.
on
This post builds upon the previous post for getting started with Unit Testing AngularJS. We established the basics of Jasmine and finished up with a set of unit tests using simple calculator functionality as an example. Here’s the code:
<html>
<head>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.3/jasmine.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.3/jasmine.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.3/jasmine-html.min.js"></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jasmine/2.3.3/boot.min.js"></script>
</head>
<body>
</body>
<script type="text/javascript">
// Basic calculator logic
var calculator = {
sum: function (x, y) {
return x + y;
},
subtract: function (x, y) {
return x - y;
},
divide: function (x, y) {
return (y === 0) ? 0 : x / y;
}
}
// The tests
describe('calculator', function () {
describe('sum', function () {
it('1 + 1 should equal 2', function () {
expect(calculator.sum(1, 1)).toBe(2);
});
});
describe('subtract', function () {
it('3 - 2 should equal 1', function () {
expect(calculator.subtract(3, 2)).toBe(1);
});
});
describe('divide', function () {
it('10 / 5 should equal 2', function () {
expect(calculator.divide(10, 5)).toBe(2);
});
it('zero divisor should equal 0', function () {
expect(calculator.divide(10, 0)).toBe(0);
});
});
});
</script>
</html>
We are going to build upon this and factor in AngularJS, and how we would go about unit testing this functionality if it was within an Angular controller. Don’t worry if you’ve yet to get started with Angular, this will be a good opportunity pick up some of the core concepts.
A Basic Angular App
Before we consider the tests, let’s consider a simple angular version of the calculator. It has the basic sum funtionality we saw previously. It looks as follows in the browser:
Here is the code for the calculator app:
<html>
<head>
<script type="text/javascript" src="https://code.angularjs.org/1.4.0-rc.2/angular.min.js"></script>
</head>
<body>
<!-- This div element corresponds to the CalculatorController we created via the JavaScript-->
<div ng-controller="CalculatorController">
<input ng-model="x" type="number">
<input ng-model="y" type="number">
<strong>{{z}}</strong>
<!-- the value for ngClick maps to the sum function within the controller body -->
<input type="button" ng-click="sum()" value="+">
</div>
</body>
<script type="text/javascript">
// Creates a new module called 'calculatorApp'
angular.module('calculatorApp', []);
// Registers a controller to our module 'calculatorApp'.
angular.module('calculatorApp').controller('CalculatorController', function CalculatorController($scope) {
$scope.z = 0;
$scope.sum = function() {
$scope.z = $scope.x + $scope.y;
};
});
// load the app
angular.element(document).ready(function() {
angular.bootstrap(document, ['calculatorApp']);
});
</script>
</html>
The key concepts of the code presented:
1. Creating a module
What’s angular.module? It’s a global place for creating, registering and retrieving Angular modules. This line creates a new module called calculatorApp, we will add components to this module:
angular.module('calculatorApp', []);
What’s the second [] argument? The second parameter needs to be present to indicate that we are creating a new module. If our app required angular dependencies, we would pass them here e.g. [‘ngResource’, ‘ngCookies’]. The absence of the second parameter indicates that this is a get request to return the module instance.
Conceptually, it would have been clearer to name these two concepts:
* angular.module.createInstance(name, requires);
* angular.module.getInstance(name);
But we have:
* angular.module('calculatorApp', []); // i.e. createInstance
* angular.module('calculatorApp'); // i.e. getInstance
For more information see the angular.module documentation.
2. Adding a controller to the module
Following on from the previous point, we make use of the module instance via angular.module(‘calculatorApp’) to add components to it. The code:
angular.module('calculatorApp').controller('CalculatorController', function CalculatorController($scope) {
$scope.z = 0;
$scope.sum = function() {
$scope.z = $scope.x + $scope.y;
};
});
A controller is where the code to combine the view with the business logic should be placed. The variable $scope acts as the glue between the controller and the HTML view.
3. Connecting HTML elements to the controller
The following HTML represents the input fields we need for the calculator, and links the containing div to the controller.
<div ng-controller="CalculatorController">
<input ng-model="x" type="number">
<input ng-model="y" type="number">
<strong>{{z}}</strong>
<!-- the value for ngClick maps to the sum function within the controller body -->
<input type="button" ng-click="sum()" value="+">
</div>
The input elements with the ng-model attribute are linked to the scope properties of the same name in the controller. We also make use of ng-click to link the button to the function of the same ‘sum’ that’s on the scope object in the controller.
Adding Tests
Now that we’ve established our basic app, we next need to think about how we go about unit testing this? We’re unit testing, so we ignore the HTML we had in the example app and focus on the CalculatorController code:
angular.module('calculatorApp').controller('CalculatorController', function CalculatorController($scope) {
$scope.z = 0;
$scope.sum = function() {
$scope.z = $scope.x + $scope.y;
};
});
In order to test the controller we must address the following points:
- How do we create an instance of the controller?
- How can we get/set properties on the scope object?
- How can we invoke functions (i.e. the sum() function) on the scope object?
Here’s the test code that addresses these points (or see this gist of the code for the full page):
describe('calculator', function () {
beforeEach(angular.mock.module('calculatorApp'));
var $controller;
beforeEach(angular.mock.inject(function(_$controller_){
$controller = _$controller_;
}));
describe('sum', function () {
it('1 + 1 should equal 2', function () {
var $scope = {};
var controller = $controller('CalculatorController', { $scope: $scope });
$scope.x = 1;
$scope.y = 2;
$scope.sum();
expect($scope.z).toBe(3);
});
});
});
Note: as per the last post, to run these tests we have to run this as part of an HTML page in the browser.
To start with we need to add a reference to ngMock. This adds the angular.mock object that’s used in the new test code. The ngMock module provides a mechanism to inject and mock services for unit tests.
How do we get an instance of the controller?
Using the ngMock functionality we can register an instance of the calculator app:
beforeEach(angular.mock.module('calculatorApp'));
Once the ‘calculatorApp’ module has been initialised, we can call the inject function, so that we can resolve a reference of the $controller service:
beforeEach(angular.mock.inject(function(_$controller_) {
$controller = _$controller_;
}));
Now that the app is loaded, and we’ve used the inject function we can use the $controller service to get an instance of our CalculatorController controller:
var controller = $controller('CalculatorController', { $scope: $scope });
How can we get/set properties on the scope object?
In the last snippet we saw how we could get an instance of the CalculatorController. The second parameters in the curly braces are the arguments of the controller function itself. Our controller has a single argument; the $scope object:
function CalculatorController($scope) { ... }
For our test, the $scope object can be represented by a simple JavaScript object:
var $scope = {};
var controller = $controller('CalculatorController', { $scope: $scope });
// set some properties on the scope object
$scope.x = 1;
$scope.y = 2;
Setting the properties of x and y emulates what we saw in the animated gif that demonstrated the app running in the browser.
We can also read properties of the object as we see in the assertion of the test:
expect($scope.z).toBe(3);
How can we invoke functions on the scope object?
The final thing to consider is how to emulate the user clicking the ‘+’ button as demonstrated in the animated gif. All we need to do is invoke the function as we would any regular JavaScript function:
$scope.sum();
Summary
In this post we saw a basic unit test for an angular controller where the running of the tests required us to open/refresh the browser manually. We can improve upon this workflow, which is discussed in this post: Getting started with Karma for AngularJS Testing