AngularJS Testing
This article is part of my personal wiki where I write personal notes while I am learning new technologies. You are welcome to use it for your own learning!
Setting Up Karma
Karma is a testing automation tool developed by the angular team. It used to be called Testacular and was developed initially by one of the core angular team members Misko Hevery.
You can install Karma using npm by installing the following packages in your angular project:
# install karma
# karma chrome adapter that let's you run tests on chrome
# karma jasmine adapter that let's you write tests with jasmine
PS> npm install karma karma-chrome-launcher karma-jasmine
And the Karma CLI tool globally:
PS> npm install -g karma-cli
Once added to your project, you’ll need to create a suitable karma configuration for your project where you’ll tell karma where the JavaScript files and specs reside. You can create a new configuration file using the karma CLI and following the instructions:
PS> karma init
You can use globs to select several files at the same time:
// configs...
files: [
'lib/angular/angular.js',
'lib/angular/angular-*.js',
//...
'js/**/*.js',
'specs/**/*.spec.js',
// etc
]
// more configs...
Once you have a suitable test configuration you can run karma using the following command:
PS> karma start karma.conf.js
Testing Angular Controllers
AngularJS provides the angular-mocks
module that helps you mockup all angular dependencies within you application and get references to Angular components such as controllers, service, factories, etc.
If we had a simple controller that uses a service to grab some players and expose them to a view via the $scope
:
(function(){
angular.module('myApp'), []);
angular
.module('myApp')
.controller('MyController', function($scope, players){
$scope.players = players.get();
});
}());
We could test it like this using jasmine:
(function(){
'use strict';
describe('My controller', function(){
var $controllerFactory,
scope,
fakePlayers;
// This inializes the myApp module
beforeEach(module('myApp'));
// This allows us to get references to angular components
beforeEach(inject(function($controller, $rootScope){
$controllerFactory = $controller;
scope = $rootScope.new();
fakePlayers = sinon.stub({get: function(){}});
}));
it('should add players to the scope', function(){
// Arrange
// you could use sinon to create this fakes
var allFakePlayers = [];
fakePlayers.get.returns(allFakePlayers);
// Act
$controllerFactory('MyController',
{ '$scope': scope, players: fakePlayers };
// Assert
expect(scope.players).toBe(allFakePlayers);
});
});
}());
In this example we use the module
function to initialize the myApp
module. We also use the inject
function to inject angular components as arguments to a function callback that we can later use in our tests. For instance, in the test above we inject the $controller
factory that will let us instantiate controllers within our tests, and the $rootScope
that let’s us create scope
objects.
Since we are only interested in testing the controller itself, we need to mock or stub all its dependencies and inject them into the controller. In order to do that we instantiate a clean scope
and create a fake of the players
service using sinon.js.
Using the $controllerFactory
, the scope
and the fake fakePlayers
service we can instantiate the controller MyController
and test it. In this particular test we want to assert that when insantiating a new controller the scope
gets a property players
that is set to the players obtained via fakePlayers.get
method. In order to be able to verify this, we setup the sinon fake to return a series of fake players when the method get
is called. Using jasmine expect
function we can finally verify that the scope has the right property with the expected players. And thus complete our unit test.
In a similar fashion, if we have a method that saves a given player profile:
(function(){
angular.module('myApp'), []);
angular
.module('myApp')
.controller('MyController', function($scope, players){
$scope.players = players.get();
$scope.savePlayer = function(player){
players.save(player);
};
});
}());
we can create a similar test but this time with a mocked players
service and a test where we will verify that the save
method was called on the service with the given player
argument:
(function(){
'use strict';
describe('My controller', function(){
var $controllerFactory,
scope,
fakePlayers;
// This inializes the myApp module
beforeEach(module('myApp'));
// This allows us to get references to angular components
beforeEach(inject(function($controller, $rootScope){
$controllerFactory = $controller;
scope = $rootScope.new();
fakePlayers = sinon.stub({get: function(){}, save: function(){}});
}));
it('should add players to the scope', function(){
// same as before
});
describe('when attempting to save a player', function(){
it('save the player via the players service', function(){
// Arrange
// you could use sinon to create this fakes
var player = '@vintharas';
ctrl = $controllerFactory('MyController',
{ '$scope': scope, players: fakePlayers };
// Act
scope.save(player);
// Assert
expect(fakePlayers.save.calledWith(player).toBe(true);
});
});
});
}());
Testing Angular Services or Factories
You can simple test services or factories by injecting them directly in your tests using the inject
function:
(function(){
'use strict';
describe('players', function(){
var $controllerFactory,
scope,
fakePlayers;
// This inializes the myApp module
beforeEach(module('myApp'));
// we use inject to let Angular instantiate the
// service or factory for us
it('should add players to the scope', inject(function(players){
// write test
})});
});
}());
This doesn’t work if your service has dependencies, because we want to have those faked so we can test the players
service in isolation. How can we write tests for services of factories that have dependencies? We need a way to tell angular to inject our own fake dependencies when instantiating a service of factory. We can do that by using Angular $provide
component. Imagine that the players
service uses another service data
that does AJAX calls to our server and takes care of data access responsibilities, caching, etc. We can inject a fake version of data
as follows:
//...
beforeEach(module('myApp'));
beforeEach(function(){
var fakeData = sinon.stub({getPlayers: function(){}});
module(function($provide){
$provide.value('data', fakeData);
});
});
//...
Using the $provide.data
function we tell Angular to inject our fakeData
object whenever an Angular component needs the data
service.
Testing AJAX and Async
Angular provides great support for testing AJAX requests done via both $http
and $resource
through a client proxy that intercepts all HTTP calls done to the server, the $httpBackend
service.
to be continued…
Testing Filters
Testing Directives
References
Written by Jaime González García , dad, husband, software engineer, ux designer, amateur pixel artist, tinkerer and master of the arcane arts. You can also find him on Twitter jabbering about random stuff.