AngularJS Testing with Karma and Jasmine

AngularJS-Shield-largeAngularJS is the best thing to happen to JavaScript since jQuery. It’s what JavaScript development has always wanted to be. One of the key advantages to Angular is its dependency injection which is very advantageous when you want to unit test your code. There is one little quirk though… I can’t for the life of me find a tutorial out there that shows how to do that unit testing.

Sure there are recommendations: use the Jasmine test framework with the Karma test runner; but there isn’t a start to finish setup guide to make testing work. So I made one. I had to go all around the web finding out how to do this, which (if this is your first stop) you won’t have to do.

If you notice any errors please let me know, but as far as I can tell this is the best way to unit test Angular with Karma and Jasmine.

Introduction

This tutorial will lead you through installation of all the tools you will need to run automated tests using Karma and Jasmine. I don’t care if you’re doing TDD or TAD, but for this example, we’ll assume that you already have a file you want to test.

Install Karma

If you don’t have node.js installed, download and install it. After you have it installed go to your terminal or command line and type:

npm install -g karma

File structure

The file structure is irrelevant, but for these tests it will look something like this:

Application
| angular.js
| angular-resource.js
| Home
  | home.js
| Tests
  | Home
    | home.tests.js
  | karma.config.js (will be created in the next step)
  | angular-mocks.js

* I’m not advocating this file structure, I simply show it for example sake.

Configure Karma

Create a configuration file by navigating to the directory you wish it to be in and typing the following command in your terminal:

karma init karma.config.js

You’ll be asked a few questions including which testing framework you want to use, whether you want the files to be auto watched, and what files to include. For our tutorial we’ll leave ‘jasmine’ as the default framework, let it autowatch files, and include the following files:

../*.js
../**.*.js
angular-mocks.js
**/*.tests.js

These are relative paths that include 1) any .js file in the parent directory, any .js file inside of any directory inside of the parent directory, angular-mocks.js, and any file within any directory (located in the current directory) that is formated [name].tests.js (which is how I like to delineate test file from other files).

Whatever files you choose, just be sure that you include angular.js, angular-mocks.js, and any other files that you’ll need.

Start Karma

Now you are ready to start Karma. Again from the terminal type:

karma start karma.config.js

This will start any browsers you listed in the config file on your computer. Each browser will be connected to the Karma instance with it’s own socket and you will see a list of active browsers that will tell you whether or not it is running tests. I wish that Karma would tell you a summary of the last result of your tests for each browser (15 out of 16 passed, 1 failed) but alas for that information you need to look at the terminal window.

An awesome thing about Karma is that you can test on any device connected to your network. Try pointing your phone’s browser to Karma by looking at teh URL of one of the browser windows running the tests. It should look something like this: http://localhost:9876/?id=5359192. Point your phone, VM, or any other device with a browser to [your network IP address]:9876/?id=5359192. Because Karma is running an instance of node.js, your test machine is acting like a server and will send the tests to any browser that is pointed to it.

Make Basic Test

We are assuming that you already have a file to test. We’ll say that your home.js file looks something like this:

home.js

1
2
3
4
5
6
7
8
9
10
11
12
'use strict';
 
var app = angular.module('Application', ['ngResource']);
 
app.factory('UserFactory', function($resource){
    return $resource('Users/users.json')
});
 
app.controller('MainCtrl', function($scope, UserFactory) {
    $scope.text = 'Hello World!';
    $scope.users = UserFactory.get();
});

Inside of home.tests.js we can create our tests cases. We’ll start out with the simpler of the two: $scope.text should equal ‘Hello World!’. To test this we must mockout our Application module and the $scope variable. We’ll do this in the Jasmine beforeEach function so that we’ll have a fresh controller and scope at the beginning of each test.

home.tests.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
'use strict';
 
describe('MainCtrl', function(){
    var scope;//we'll use this scope in our tests
 
    //mock Application to allow us to inject our own dependencies
    beforeEach(angular.mock.module('Application'));
    //mock the controller for the same reason and include $rootScope and $controller
    beforeEach(angular.mock.inject(function($rootScope, $controller){
        //create an empty scope
        scope = $rootScope.$new();
        //declare the controller and inject our empty scope
        $controller('MainCtrl', {$scope: scope});
    });
    // tests start here
});

You’ll see in the code example that we are injecting our own scope so that we can verify information off of it. Also, do not forget to mock out the module itself as on line 7! We are now ready to do our tests:

home.tests.js

15
16
17
18
    // tests start here
    it('should have variable text = "Hello World!"', function(){
        expect(scope.text).toBe('Hello World!);
    });

If you run this test it should run in any browsers looking at Karma and pass.

Make $resource Request

Now we’re ready to test the $resource request. To make this request we need to use $httpBackend with is a mocked out version of Angular’s $http. We’ll create another variable called $httpBackend and in our second beforeEach block we’ll inject _$httpBackend_ and assign the new variable to _$httpBackend_. We’ll then tell $httpBackend how to respond to requests.

10
11
        $httpBackend = _$httpBackend_;
        $httpBackend.when('GET', 'Users/users.json').respond([{id: 1, name: 'Bob'}, {id:2, name: 'Jane'}]);

And our tests:

home.tests.js

20
21
22
23
24
    it('should fetch list of users', function(){
            $httpBackend.flush();
            expect(scope.users.length).toBe(2);
            expect(scope.users[0].name).toBe('Bob');
        });

All Together

home.tests.js

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
'use strict';
 
describe('MainCtrl', function(){
    var scope, $httpBackend;//we'll use these in our tests
 
    //mock Application to allow us to inject our own dependencies
    beforeEach(angular.mock.module('Application'));
    //mock the controller for the same reason and include $rootScope and $controller
    beforeEach(angular.mock.inject(function($rootScope, $controller, _$httpBackend_){
        $httpBackend = _$httpBackend_;
        $httpBackend.when('GET', 'Users/users.json').respond([{id: 1, name: 'Bob'}, {id:2, name: 'Jane'}]);
 
        //create an empty scope
        scope = $rootScope.$new();
        //declare the controller and inject our empty scope
        $controller('MainCtrl', {$scope: scope});
    });
    // tests start here
    it('should have variable text = "Hello World!"', function(){
        expect(scope.text).toBe('Hello World!');
    });
    it('should fetch list of users', function(){
        $httpBackend.flush();
        expect(scope.users.length).toBe(2);
        expect(scope.users[0].name).toBe('Bob');
    });
});

Tips

  • Karma will run all tests in all files, if you only want to run a subset of tests change describe or it to ddescribe or iit to run the respective tests. If there are tests that you do not want to test change describe or it to xdescribe or xit to ignore that set of code.
  • I would also suggest reading through the Jasmine documentation to know what methods are available to you.
  • You also have the option to run your tests in an html file on the page. The code for our example would look something like this:

    home.runner.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    <!DOCTYPE html>
    <html>
    <head>
        <title>Partner Settings Test Suite</title>
        <!-- include your script files (notice that the jasmine source files have been added to the project) -->
        <script type="text/javascript" src="../jasmine/jasmine-1.3.1/jasmine.js"></script>
        <script type="text/javascript" src="../jasmine/jasmine-1.3.1/jasmine-html.js"></script>
        <script type="text/javascript" src="../angular-mocks.js"></script>
        <script type="text/javascript" src="home.tests.js"></script>
        <link rel="stylesheet" href="../jasmine/jasmine-1.3.1/jasmine.css"/>
    </head>
    <body>
        <!-- use Jasmine to run and display test results -->
        <script type="text/javascript">
            var jasmineEnv = jasmine.getEnv();
            jasmineEnv.addReporter(new jasmine.HtmlReporter());
            jasmineEnv.execute();
        </script>
    </body>
    </html>

40 Comments

  1. Nice job and thanks.
    I think there should be an additional ‘)’ in the first home.tests.js line 14, making it })); rather than });

  2. It appears that angular-mocks.js only defines angular.mock.module when Jasmine is being used. I don’t understand why it would exclude the use of Mocha.

  3. Do we actually need karma if we are performing tests using browser with all jasmine files included?

    I kind of found the fact that karma opens browser completely redundant – as it doesn’t really do anything apart from using it’s console – all the relevant information is displayed in the terminal anyway.

  4. Also – you haven’t mentioned what the users.json file should contain – I’m constantly getting the following test failure:

    TypeError: Object # has no method ‘push’

  5. @Mark
    You can use a headless browser such as PhantomJS with karma to avoid a browser window opening.

  6. I tried running a simple test. But its failing with below error:
    C:\Users\502245602\combo>karma start src/main/webapp/karma.config.js
    INFO [karma]: Karma v0.10.4 server started at http://localhost:9876/
    INFO [launcher]: Starting browser Chrome
    INFO [Chrome 30.0.1599 (Windows 7)]: Connected on socket auZijvn9LzbH_2K0QDcv
    Chrome 30.0.1599 (Windows 7) Sanity Test Sanity test Jasmine” FAILED
    expect undefined toEqual “Hi”
    …./src/main/webapp/test/spec/controllers/welcome.test.js:15:3: expected “Hi” but was undefined
    Chrome 30.0.1599 (Windows 7): Executed 1 of 1 (1 FAILED) ERROR (0.38 secs / 0.024 secs)

    my test looks like below:

    describe(‘Sanity Test’, function() {
    var scope;
    beforeEach(angular.mock.module(‘serviceApp’));
    beforeEach(angular.mock.inject(function($rootScope, $controller) {
    scope = $rootScope.$new();
    $controller(‘welcomeController’, {
    $scope : scope
    });
    }));

    it(‘Sanity test Jasmine”‘, function() {
    scope.text = ‘Hi’;
    expect(‘Hi’).toEqual(‘Hi’);
    });
    });

    any help…

  7. Could not get html runner working:

    Uncaught TypeError: Cannot set property ‘mock’ of undefined angular-mocks.js:17
    Uncaught TypeError: undefined is not a function home.runner.html:16

  8. Also the test did not pass:

    Chrome 31.0.1650 (Mac OS X 10.8.5) MainCtrlTest should have variable text = “Hello World!” FAILED
    ReferenceError: $httpBackend is not defined
    at null. (/Users/dmitrizaitsev/Dropbox/Priv/APP/Testers/generated-app-test/test/spec/home.tests.js:21:26)
    at Object.invoke (/Users/dmitrizaitsev/Dropbox/Priv/APP/Testers/generated-app-test/app/bower_components/angular/angular.js:3697:17)
    at workFn (/Users/dmitrizaitsev/Dropbox/Priv/APP/Testers/generated-app-test/app/bower_components/angular-mocks/angular-mocks.js:2102:20)
    Error: Declaration Location
    at Object.window.inject.angular.mock.inject [as inject] (/Users/dmitrizaitsev/Dropbox/Priv/APP/Testers/generated-app-test/app/bower_components/angular-mocks/angular-mocks.js:2087:25)
    at null. (/Users/dmitrizaitsev/Dropbox/Priv/APP/Testers/generated-app-test/test/spec/home.tests.js:19:29)
    at /Users/dmitrizaitsev/Dropbox/Priv/APP/Testers/generated-app-test/test/spec/home.tests.js:3:1
    TypeError: Cannot read property ‘text’ of undefined
    at null. (/Users/dmitrizaitsev/Dropbox/Priv/APP/Testers/generated-app-test/test/spec/home.tests.js:32:21)
    Chrome 31.0.1650 (Mac OS X 10.8.5): Executed 2 of 2 (1 FAILED) (0.031 secs / 0.028 secs)

    However, dropping the _ miraculously works:

    function($rootScope, $controller, $httpBackend){
    $httpBackend.when(‘GET’, ‘Users/users.json’).respond([{id: 1, name: 'Bob'}, {id:2, name: 'Jane'}]);

    Chrome 31.0.1650 (Mac OS X 10.8.5): Executed 2 of 2 SUCCESS (0.039 secs / 0.035 secs)

  9. How to test multiple controller, whether i have to declare ‘var scope’ for each controller ?

  10. How to test multiple controller, whether i have to declare ‘var scope’ for each controller?

  11. Really nice!

    I spend days to make my tests work with karma/jasmine, and this is the best article that i found

  12. @Dmitri

    About the undefined mock,
    make sure your config/karma.conf.js
    is pointing at the right file location.

    Example:

    files : [
    'public/js/lib/angular/angular.js',
    'public/js/lib/angular/angular-*.js',
    'test/lib/angular/angular-mocks.js',
    'public/js/**/*.js',
    'test/unit/**/*.js'
    ]

  13. I was unable to get this example to work with a fresh install, even after fixing the above errors.
    I’m using the latest versions of all tools, and Angular is v1.2.15
    The first error was:
    Error: [$injector:modulerr] Failed to instantiate module ngResource due to:
    Error: [$injector:nomod] Module ‘ngResource’ is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.

    So I simplified the example further to:
    appl.js
    =====

    'use strict';
    var app = angular.module('Application');
    app.controller('MainCtrl', function($scope) {
    $scope.text = 'Hello World!';
    });

    ——–
    appl.test.js
    ==========

    'use strict';
    describe('MainCtrl', function() {
    var scope;//we'll use this scope in our tests
    beforeEach(angular.mock.module('Application'));
    //mock the controller for the same reason and include $rootScope and $controller
    beforeEach(angular.mock.inject(function($rootScope, $controller){
    scope = $rootScope.$new();
    $controller('MainCtrl', {$scope: scope});
    }));
    it('should have variable text = "Hello World!"', function() {
    expect(scope.text).toBe('Hello World!');
    });
    });

    ————-
    but this fails with:
    Uncaught Error: [$injector:nomod] Module ‘Application’ is not available! You either misspelled the module name or forgot to load it. If registering a module ensure that you specify the dependencies as the second argument.

    Any help is greatly appreciated…

  14. Hey P Snider, I had the same problem– it looks like the home.js file isn’t being included in the Karma config file (that’s why it couldn’t find the Application Module), so in karma.config.js, I added ‘../Home/*.js’ to the files array.

  15. Great tutorial.

    Couple of addition to it will make a real life testing suite:

    1: PhantomJS integration for headless ( no opening browsers anymore) testing.
    2: Coverage, for testing code coverage in the application.
    3: Switching auto-run to false and doing on demand tests. (depends across type of projects and developers)

    All in all a great place to start karma-jasmine-angular unit testing !!!!

  16. Note that it is now recommended to globally install karma-cli (npm install -g karma-cli) which will take care of fetching the appropriate karma.

    Thus you can install a different local version specific to each project and karma-cli will pick the appropriate one.

  17. To run the command “karma init karma.config.js” I had to install the package karma-cli (npm install -g karma-cli)

  18. Great article. Thanks!

    But I think that the whole point of factories is to isolate the dependancies, so your controller doesn’t care uf the users are created by http request or its a simply hardcoded array. I think that the most useful way to test this funcionality is to stub the userfactory with an array or a mock promise that resolves returning the users array. What do you think?

  19. I see a lot of interesting posts on your page.

    You have to spend a lot of time writing,
    i know how to save you a lot of work, there is a tool
    that creates unique, google friendly posts in couple of minutes, just type in google – laranita’s free content
    source

  20. Hi, I’ve tried exactly the same, but I’m always getting these following Errors:

    minErr/<@/home/michael/webui-ng/src/client/app/bower_components/angular/angular.js:78:5
    loadModules/<@/home/michael/webui-ng/src/client/app/bower_components/angular/angular.js:3859:1
    forEach@/home/michael/webui-ng/src/client/app/bower_components/angular/angular.js:325:7
    loadModules@/home/michael/webui-ng/src/client/app/bower_components/angular/angular.js:3824:5
    createInjector@/home/michael/webui-ng/src/client/app/bower_components/angular/angular.js:3764:3
    workFn@/home/michael/webui-ng/src/client/app/bower_components/angular-mocks/angular-mocks.js:2150:9

    TypeError: scope is undefined in /home/michael/webui-ng/src/client/test/hello.js (line 17)
    @/home/michael/webui-ng/src/client/test/hello.js:17:9

    My idea is, that the problem has something to do with the requirements array in angular.module(‘Application’, ['ngRessource']), because when letting this empty the test passes. I’m pretty stuck at this problem at the moment, do you have an idea what it could be?

  21. For people who just want to simply test on a web page, such as in the home.runner.html example above, and using Jasmine 2.X: you’ll need the boot.js file declared after jasmine.js and jasmine-html.js.

    If not, you’ll get nothing but a couple of reference errors in the browser console.

  22. Hello There. I found your blog using msn. This is an extremely neatly written article.

    I’ll be sure to bookmark it and come back to read more of your
    useful info. Thank you for the post. I’ll definitely comeback.

  23. @ p snider
    Problem there was that you were not initializing the app module correctly

    try…
    var app = angular.module(‘Application’,[]);
    instead of…
    var app = angular.module(‘Application’);

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>