Unit testing controllers in AngularJS with karma-jasmine

June 26, 2016
by Rahil Shaikh

This is our second article on unit testing in AngularJS. Previously we had written an article on how to setup our machine and get started with unit testing, we also wrote a test for a custom filter. In this article, we will see how we can unit test controllers in AngularJS with Karma and Jasmine. We will try to make this as simple as possible as we did in our previous tutorial.
If you are getting started with Unit testing please give a read to the getting started guide linked below.

Setup and Testing Pattern

We have already covered this in the previous article(link above). Please read about the testing pattern we are using in that article as it is crucial for further understanding. We will continue with the same setup and testing pattern. Below are all the existing files.

DOWNLOAD

Folder Structure
../
    app/
        app.js //angular code here
    node_modules/
    tests/
        app.specs.js //tests here
    karma.conf.js
    package.json

 

karma.conf.js
// Karma configuration
// Generated on Sun Jun 12 2016 12:34:55 GMT+0530 (India Standard Time)
module.exports = function(config) {
  config.set({
    // base path that will be used to resolve all patterns (eg. files, exclude)
    basePath: '',
    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: ['jasmine'],
    // list of files / patterns to load in the browser
    files: [
      'node_modules/angular/angular.js',
      'node_modules/angular-mocks/angular-mocks.js',
      'app/*.js',
      'tests/*.js',
    ],
    // list of files to exclude
    exclude: [
    ],
    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
    },
    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ['progress'],
    // web server port
    port: 9876,
    // enable / disable colors in the output (reporters and logs)
    colors: true,
    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_INFO,
    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: true,
  // start these browsers
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: ['PhantomJS'],
    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: false,
    // Concurrency level
    // how many browser should be started simultaneous
    concurrency: Infinity
  })
}

 

package.json
{
  "name": "unit-testing-controllers",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "karma start"
  },
  "keywords": [],
  "author": "Rahil",
  "license": "ISC",
  "dependencies": {
    "angular": "1.5.6",
    "jasmine-core": "2.4.1"
  },
  "devDependencies": {
    "angular-mocks": "1.5.6",
    "jasmine-core": "2.4.1",
    "karma": "0.13.22",
    "karma-coverage": "1.0.0",
    "karma-jasmine": "1.0.2",
    "karma-phantomjs-launcher": "1.0.0",
    "phantomjs-prebuilt": "2.1.4"
  }
}

Roadmap

In this tutorial we will look at the following scenarios. We will majorly work with controller as syntax, but I will show you how to deal with $scope pattern as well, with a simple example.

  1. Testing in Controller As syntax
  2. Testing Methods that set property
  3. Testing Methods that return value
  4. Testing in $scope pattern
  5. Mocking external dependencies used by controller and testing the result

Testing a single property in Controller As syntax

Let us first create a controller in our app.js with a single property.

app.js
angular.module('MyApp', [])
.controller('myctrl',[function(){
    var vm = this;
    vm.mode = 'fun'; //Lets test if property name is set to Rahil
}]);

As you can see there is nothing much in our controller, just a single property vm.mode which is set to fun. Now open up app.specs.js and lets write a test to check if the property is setting properly.

app.specs.js
describe('Controllers', function(){ //describe your object type
    beforeEach(module('MyApp')); //load module<br />
    describe('myctrl',function(){ //describe your app name<br />
        var myctrl;
        beforeEach(inject(function($controller){ //instantiate controller using $controller service
            myctrl = $controller('myctrl');
        }));
        it('Mode should be fun', function(){  //write tests
            expect(myctrl.mode).toBe('fun'); //pass
        });
    });
});

As you can see above we are first describing our object type next injecting module then we describe our controller. Next we are initializing our controller and assigning it to myctrl using the $controller service.
Notice since we are using controller as syntax we will need a reference to an instance of our controller to access the methods and properties defined on it. In our case, we are assigning the instance to myctrl
Finally, we have written a simple test using the it block. To run the test, navigate into your working directory using command-line and run the below command.

$ karma start
OR
$ npm test

Testing methods that set a property

Now let’s write a method that will set the description to (child, teen, adult) based on the age group. Where we will provide age as a parameter.

app.js
angular.module('MyApp', [])
.controller('myctrl',[function(){
    var vm = this;
    vm.mode = 'fun';
    /** Adding method here */
    vm.setDescription = function(age){
        if(age &lt;= 10){
            vm.description = 'child';
        } else if(age > 10 &amp;&amp; age &lt; 18){
            vm.description = 'teen';
        } else if(age >= 18){
             vm.description = 'adult';
        }
    }
}]);

 
Now let us add the specs for the same in our app.specs.js

app.specs.js
describe('Controllers', function(){ //describe your object type
    beforeEach(module('MyApp')); //load module
    describe('myctrl',function(){ //describe your app name
        beforeEach(inject(function($controller){ //instantiate controller using $controller service
            myctrl = $controller('myctrl');
        }));
        it('Mode should be fun', function(){  //write tests
            expect(myctrl.mode).toBe('fun'); //pass
        });
        /** Specs to test vm.description */
        it('Should set desciption according to age', function(){
            myctrl.setDescription(4); //calling the method with age=4
            expect(myctrl.description).toBe('child'); //testing the property
            myctrl.setDescription(15); //calling the method with age=15
            expect(myctrl.description).toBe('teen');
            myctrl.setDescription(54); //calling the method with age=54
            expect(myctrl.description).toBe('adult');
        });
    });
});

Inside our it block we are first calling the method myctrl.setDescription(age) each time before expecting the property myctrl.description to be set appropriately.

Testing Methods that return value

Let us write a simple method that takes in two numbers and returns us the added value.
Open up app.js and add the following.

app.js
vm.add = function(a,b){
        if(typeof a !== 'number' || typeof b !== 'number'){
            return 'invalid args';
        }
        return a+b;
    }

 
Below are the tests for the same.

app.specs.js
 /** Specs to test vm.add() */
        it('Should add two numbers', function(){
            expect(myctrl.add(4,2)).toBe(6); //4+2 = 6
            expect(myctrl.add('abcd',2)).toBe('invalid args'); // wrong arg type
        });

 

Testing in $scope pattern

Sometimes it might be the case that you are using $scope pattern for your controllers and that all of your properties and methds would be defined on the $scope variable. Testing them is easy as well. Have a look below.

app.specs.js
        var myctrl;
        var scope;
        beforeEach(inject(function($controller, $rootScope){ //instantiate controller using $controller service and inject $rootScope service
            scope = $rootScope.$new();
            myctrl = $controller('myctrl', {
                scope : scope
            });
        }));

In your inject block where you are initializing the controller, change it to look like above. We are basically injecting $rootScope service and assigning the value of our controllers $scope to scope variable.
Now any properties or methods defined in the controller on $scope can be accessed in your specs using the scope variable.

Mocking external dependencies used by controller and testing the result

We will take an example of an asynchronous function defined on a factory on which our controller is depending. We are using this example as asynchronous functions are the trickiest to deal with while writing unit tests.
Let us go ahead and create a factory function and use it in our controller.

app.js (Factory)
angular.module('MyApp', [])
.factory('myFactory',['$q', function($q){
    return {
        fetchServerData : function(error){
            /** error is passed just as an example
            real case scenairio wolud be different and usually depend
            on the response from the web-service */

            var d = $.q.defer();
            if(error){
                d.reject('Some error occured');
            } else {
                d.resolve('Success');
            }
            return d.promise;
        }
    }
}]);

$q is used to create asynchronous functions that return promise. You can read more about them here.
Next we will call our factory method inside myctrl and set properties depending on the result.

app.js
/** Calling our asynchronous service
     * setting hasError and message properties
     * There properties must be tested in specs
     */

    myFactory.fetchServerData().then(function(response){
        //success scenario
        vm.hasError = false
        vm.message = response;
    }, function(response){
        //error scenario
        vm.hasError = true;
        vm.message = response;
    });

 
Now let us open up our specs file and write our test.
Just before where we are instantiating our controller, we need to initialise our external dependencies and spy on methods. We will also have to inject myFactory as a dependency into our controller.

app.specs.js
        var d;
        var myFactory;
        beforeEach(inject(function($q, _myFactory_){ //Mock our factory and spy on methods
            d = $q.defer();
            myFactory = _myFactory_;
            spyOn(myFactory, 'fetchServerData').and.returnValue(d.promise);
        }));
        var myctrl;
        var scope;
        beforeEach(inject(function($controller, $rootScope){ //instantiate controller using $controller service
            scope = $rootScope.$new();
            myctrl = $controller('myctrl', {
                myFactory : myFactory, //inject factory
                scope : scope
            });
        }));

Since our fetchServerData is asynchronous and returns a promise. We need to mock it to replicate a similar behaviour insde our specs using the $q service.
Using Jasmines spyOn method we can track our methods and replicate actual implementations.
Now that we are spying our method we can writes assertions to test the same.

app.specs.js
        describe('Asyn call', function() {
            it('should call fetchServerData on myFactory', function() {
                expect(myFactory.fetchServerData).toHaveBeenCalled();
                expect(myFactory.fetchServerData.calls.count()).toBe(1);
            });
            it('should do something on success', function() {
                d.resolve('Success'); // Resolve the promise to replicate success scenario.
                scope.$digest();
                // Check for state on success.
                expect(myctrl.hasError).toBe(false); //testing properties
                expect(myctrl.message).toBe('Success');
            });
            it('should do something on error', function() {
                d.reject('Some error occured'); // Reject the promise to emulate error scenario.
                scope.$digest();
                // Check for state on error.
                expect(myctrl.hasError).toBe(true);
                expect(myctrl.message).toBe('Some error occured');
            });
        });

We are first checking that our method is called and the number of times it is called.
Next, we are testing the two possible scenarios ie. Success and Failure. In which we are validating that our properties are accurately set depending on success or error.

Final Files

Our complete app.js and app.specs.js files look like this.

app.js
       angular.module('MyApp', [])
.controller('myctrl',['myFactory', function(myFactory){
    var vm = this;
    vm.mode = 'fun';
    vm.setDescription = function(age){
        if(age &lt;= 10){
            vm.description = 'child';
        } else if(age > 10 &amp;&amp; age &lt; 18){
            vm.description = 'teen';
        } else if(age >= 18){
             vm.description = 'adult';
        }
    }
    vm.add = function(a,b){
        if(typeof a !== 'number' || typeof b !== 'number'){
            return 'invalid args';
        }
        return a+b;
    }
    /** Calling our asynchronous service
     * setting hasError and message properties
     * There properties must be tested in specs
     */

    myFactory.fetchServerData().then(function(response){
        vm.hasError = false
        vm.message = response;
    }, function(response){
        vm.hasError = true;
        vm.message = response;
    });
}])
.factory('myFactory',['$q', function($q){
    return {
        fetchServerData : function(error){
            /** error is passed just as an example
            real case scenairio wolud be different and usually depend
            on the response from the web-service */

            var d = $.q.defer();
            if(error){
                d.reject('Some error occured');
            } else {
                d.resolve('Success');
            }
            return d.promise;
        }
    }
}]);
app.specs.js
describe('Controllers', function(){ //describe your object type
    beforeEach(module('MyApp')); //load module    
    describe('myctrl',function(){ //describe your app name
        var d;
        var myFactory;
        beforeEach(inject(function($q, _myFactory_){ //Mock our factory and spy on methods
            d = $q.defer();
            myFactory = _myFactory_;
            spyOn(myFactory, 'fetchServerData').and.returnValue(d.promise);
        }));  
        var myctrl;
        var scope;
        beforeEach(inject(function($controller, $rootScope){ //instantiate controller using $controller service
            scope = $rootScope.$new();
            myctrl = $controller('myctrl', {
                myFactory : myFactory, //inject factory
                scope : scope
            });
        }));        
        it('Mode should be fun', function(){  //write tests
            expect(myctrl.mode).toBe('fun'); //pass
        });
        /** Specs to test vm.description */
        it('Should set desciption according to age', function(){
            myctrl.setDescription(4); //calling the method with age=4
            expect(myctrl.description).toBe('child'); //testing the property          
            myctrl.setDescription(15); //calling the method with age=15
            expect(myctrl.description).toBe('teen');        
            myctrl.setDescription(54); //calling the method with age=54
            expect(myctrl.description).toBe('adult');
        });
         /** Specs to test vm.add() */
        it('Should add two numbers', function(){
            expect(myctrl.add(4,2)).toBe(6); //4+2 = 6          
            expect(myctrl.add('abcd',2)).toBe('invalid args'); // wrong arg type
        });    
        describe('Asyn call', function() {
            it('should call fetchServerData on myFactory', function() {
                expect(myFactory.fetchServerData).toHaveBeenCalled();
                expect(myFactory.fetchServerData.calls.count()).toBe(1);
            });
            it('should do something on success', function() {
                d.resolve('Success'); // Resolve the promise to replicate success scenario.
                scope.$digest();
                // Check for state on success.
                expect(myctrl.hasError).toBe(false); //testing properties
                expect(myctrl.message).toBe('Success');
            });
            it('should do something on error', function() {
                d.reject('Some error occured'); // Reject the promise to emulate error scenario.
                scope.$digest();
                // Check for state on error.
                expect(myctrl.hasError).toBe(true);
                expect(myctrl.message).toBe('Some error occured');
            });
        });
    });    
});

All the files can be downloaded from here.

Conclusion

In our previous tutorial, we had already learnt on getting started with unit testing in AngularJS. In this tutorial, we took our skills a step further by learning all possible scenarios of unit testing controllers in AngularJS. Hopefully testing any controller would not be a problem from now on.

About

Engineer. Blogger. Thinker. Loves programming and working with emerging tech. We can also talk on Football, Gaming, World Politics, Monetary Systems.

Free PDF

Subscribe and get AngularJS Tips PDF. We never spam!
First Name:
Email:

10 comments

  1. Noel da Costa
    |

    Hi. Same comment from me as for your previous tutorial – please could you show how to get this working with some basic dependency injection on the module, like ‘ngSanitize’ for example.

    • |

      Hey, it’s simple. Whatever dependencies you are injecting in your module. You must add those file paths in the file array in karma.conf.js so that it can load it in browser.

      eg:

       // list of files / patterns to load in the browser
          files: [
            'node_modules/angular/angular.js',
            'node_modules/angular-mocks/angular-mocks.js',
            'node_modules/angular-sanitize/angular-sanitize.js',
            'app/*.js',
            'tests/*.js',
          ],
  2. Joh
    |
    Error: [$injector:unpr] Unknown provider: $scopeProvider &lt;- $scope &lt;- MyCtrl
    http://errors.angularjs.org/1.4.8/$injector/unpr?p0=%24scopeProvider%20%3C-%20%24scope%20%3C-%20UserProfileCtrl (line 4334)
    /app/bower_components/angular/angular.js:4334:86
    [email protected]/app/bower_components/angular/angular.js:4482:46
    /app/bower_components/angular/angular.js:4339:48
    [email protected]/app/bower_components/angular/angular.js:4482:46
    [email protected]/app/bower_components/angular/angular.js:4514:23
    [email protected]/app/bower_components/angular/angular.js:4531:33
    /app/bower_components/angular/angular.js:9197:39
    /app/bower_components/angular-mocks/angular-mocks.js:1882:21
    /test/spec/controllers/user-profile_spec.js:7:40
    [email protected]/app/bower_components/angular/angular.js:4523:22
    [email protected]/app/bower_components/angular-mocks/angular-mocks.js:2439:26
    [email protected]://localhost:9876/context.js:151:17
    undefined&quot;,&quot;TypeError: undefined is not an object (evaluating &#039;MyCtrl.test&#039;) in /test/spec/controllers/my-ctrl_spec.js://localhost:9876/base/test/spec/controllers/my-ctrl_spec.js?0702f16b4da90c3682337df34fe227401465111f:11:33
    [email protected]://localhost:9876/context.js:151:17
    • |

      Hi joh, what exactly are you doing when you get this?
      Or are you just running the same code in the tutorial?

      • Joh
        |

        describe(‘Controllers’, function(){ //describe your object type
        beforeEach(module(‘MyApp’)); //load module
        describe(‘myctrl’,function(){ //describe your app name
        var myctrl;
        beforeEach(inject(function($controller){ //instantiate controller using $controller service
        myctrl = $controller(‘myctrl’);
        }));
        it(‘Mode should be fun’, function(){ //write tests
        expect(myctrl.mode).toBe(‘fun’); //pass
        });
        });
        });

  3. Taruna
    |

    I have followed all the steps of your last tutorial (angularjs unit testing with Karma-jasmine) and my tests ran successfully also. But as mentioned here in the beginning i can’t see package.json in my working directory folder structure. Where does that file come from ? Please reply.

    • |

      You can just use the package.json provided here and run npm install OR you can use npm init --yes and foloow the rest of the steps.

  4. |

    Hello Rahil, I am new to jasmine and karma. I do not know how to write test cases for the email validation. could you please help me out

  5. Nathan Agersea
    |

    I also ran into the unknown provider error. The bug is in the scope definition of the controller. It should be $scope. So, for the $scope pattern example:

    var myctrl;
    var scope;
    beforeEach(inject(function($controller, $rootScope){
    scope = $rootScope.$new();
    myctrl = $controller(‘myctrl’, {
    $scope : scope
    });
    }));

    Then reference scope in your specs (it assertions).

  6. Nathan Agersea
    |

    Here is a full describe block demonstrating how I got this to work:

    describe("mainController", function () {
        var controller, scope;
        beforeEach(function () {
            module("app"); //load module
        });

        beforeEach(inject(function ($controller, $rootScope) {
            scope = $rootScope.$new();
            controller = $controller("mainController", { $scope: scope });
        }));    

        it("$scope.active.nav defaults to invoices", function () {
            expect(scope.active.nav).toEqual("invoices");
        });

        it("$scope.active.list defaults to unpaid", function () {
            expect(scope.active.list).toEqual("unpaid");
        });

        it("$scope.active.tab defaults to empty string", function () {
            expect(scope.active.tab).toEqual("");
        });

        it("$scope.active.view defaults to empty string", function () {
            expect(scope.active.view).toEqual("");
        });
         
        it("list defaults to invoices", function () {
            expect(scope.current.list).toEqual({ name: "client-ach-debits-lists", params: { id: "all" }, options: { reload: false } });
        });

        it("list defaults to unpaid", function () {
            expect(scope.current.tab).toEqual({});
        });        
    });

Leave a Comment