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:

1
2
3
4
# 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:

1
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:

1
PS> karma init

You can use globs to select several files at the same time:

1
2
3
4
5
6
7
8
9
10
11
12
// 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:

1
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:

1
2
3
4
5
6
7
8
9
10
(function(){

angular.module('myApp'), []);

angular
    .module('myApp')
    .controller('MyController', function($scope, players){
        $scope.players = players.get();
    });
}());

We could test it like this using jasmine:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
(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:

1
2
3
4
5
6
7
8
9
10
11
12
13
(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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
(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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(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:

1
2
3
4
5
6
7
8
9
10
11
12
//...

    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

Comments