AngularJS Testing with Karma and Jasmine
AngularJS 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
'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
'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
// 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.
$httpBackend = _$httpBackend_;
$httpBackend.when('GET', 'Users/users.json').respond([{id: 1, name: 'Bob'}, {id:2, name: 'Jane'}]);
And our tests:
home.tests.js
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
'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
orit
toddescribe
oriit
to run the respective tests. If there are tests that you do not want to test changedescribe
orit
toxdescribe
orxit
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
var complement = "Great Job!";
'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...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.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?