Setting up Karma to run unit tests in PhantomJS and Chrome
Install NodeJS
Node packages to install:
npm install -g karma-cli
npm install -g karma --save-dev
npm install -g phantomjs
npm install -g karma-jasmine --save-dev
npm install -g karma-phantomjs-launcher --save-dev
npm install -g karma-chrome-launcher --save-dev
Install the last one only if you plan to run your unit tests using Chrome; you don’t need it if you prefer running them only in PhantomJS. Now, create the karma configuration file.
karma init karma.conf.js
To run karma: karma start karma.conf.js
For code coverage:
npm install -g karma-coverage
Example Unit test:
With Jasmine, we first describe
our test suite, and then use it
function to give our test case specification. In the spec, we expect
the behaviors of our testing target. We also use beforeEach
and afterEach
to setup or tear down test environment for our spec.
Test Simple Controller
The best way to learn is to practice. Let's start with a very simple AngularJS controller, for my
Aboutpage.
'use strict';
angular.module('jiwhizWeb').controller('AboutController',
['$rootScope', '$scope',
function($rootScope, $scope) {
$rootScope.activeMenu = {
'home' : '',
'blog' : '',
'about' : 'active',
'contact' : '',
'admin' : ''
};
$rootScope.showTitle = true;
$rootScope.page_title = 'About Me';
$rootScope.page_description = 'Here is my story.';
}
]);
This is the simplest page in my blog application, and no complex logic in the controller code. It only sets the active menu item, shows the title and sets page title with description.
The unit test for this controller will be also simple. We expect the page to do exactly what we designed, which are to set the "About" menu item as active, to show the title, and to have correct page title and description.
'use strict';
describe('Controller: public/AboutController', function() {
var $rootScope, $scope, $controller;
beforeEach(module('jiwhizWeb'));
beforeEach(inject(function(_$rootScope_, _$controller_){
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
$controller = _$controller_;
$controller('AboutController', {'$rootScope' : $rootScope, '$scope': $scope});
}));
it('should make about menu item active.', function() {
expect($rootScope.activeMenu.about == 'active');
});
it('should show title.', function() {
expect($rootScope.showTitle == true);
});
it('should have correct page title.', function() {
expect($rootScope.page_title).toEqual('About Me');
});
it('should have correct page description.', function() {
expect($rootScope.page_description).toEqual('Here is my story.');
});
});
In unit test code, we call Jasmine function
describe
for our test suite, and we pass the suite title string and function of our test suite. And inside this suite function, we call Jasmine function
it
for our test specs, and we also pass the spec title string and spec function. Inside our spec functions, we call
expect
function with actual value, which is chained with a Matcher function, which takes the expected value. Here I used
toEqual
matcher function. See
Jasmine Documentation for more matcher functions and how to define your custom matcher functions.
The first beforeEach
is to load our application module - jiwhizWeb
. And the secondbeforeEach
is to inject dependencies from AngularJS, like $rootScope
. Since we want to use same variable name "$rootScope"
in our suite function, and let $rootScope
be used by our spec functions, we have to apply a trick, that is to use underscore to wrap the in-coming parameter_$rootScope_
, and AngularJS Mock will correctly resolve it to the reference to actual$rootScope
. We can use it to create our suite variable $scope
by $scope = $rootScope.$new();
. Last, we use $controller
to initialize our AboutController
and inject all dependencies.
Test Controller with Promise
'use strict';
angular.module('jiwhizWeb').controller('BlogListController',
['$rootScope', '$scope', '$timeout', 'WebsiteService',
function($rootScope, $scope, $timeout, WebsiteService) {
$rootScope.activeMenu = {
'home' : '',
'blog' : 'active',
'about' : '',
'contact' : '',
'admin' : ''
};
$rootScope.showTitle = true;
$rootScope.page_title = 'My Personal Blog';
$rootScope.page_description = 'Some of my thoughts and experiences.';
var setup = function( pageNumber ) {
WebsiteService.load()
.then( function( websiteResource ) {
return websiteResource.$get('blogs', {'page': pageNumber, 'size':10, 'sort':null});
})
.then( function( resource )
{
$scope.page = resource.page;
$scope.page.currentPage = $scope.page.number + 1;
return resource.$get('blogPostList');
})
.then( function( blogPostList )
{
$scope.blogs = blogPostList;
blogPostList.forEach( function( blog ) {
blog.contentFirstParagraph = getFirstSection(blog.content);
blog.$get('author').then(function(author) {
blog.author = author;
});
});
})
;
};
setup(0);
$scope.selectBlogPage = function(pageNumber) {
setup(pageNumber-1);
};
}
]);
Here we have
setup
function to load list of blogs for specific page, and load each blog author. The challenge is how to test those chain of promises, which are asynchronous JavaScript operations. By searching
Stack Overflow and reading other developers'
blog, I found the way to use AngularJS
$q.defer()
to resolve the promise in unit test.
'use strict';
describe('Controller: public/BlogListController', function() {
var $rootScope, $scope;
var $controller, service;
var mockWebsite = {
$get: function(rel) {}
};
var mockResource = {
page: {
size: 10,
totalElements: 100,
totalPages: 4,
number: 0
},
$get: function(rel) {}
};
var mockBlogPostList = [
{
title: 'Test Blog',
content: '<p>This is first paragraph.</p> Other parts...',
$get: function(rel) {}
},
{
title: 'Another Blog',
content: '<p>I came second.</p>',
$get: function(rel) {}
}
];
var mockAuthor = {
displayName: 'author'
};
beforeEach(module('jiwhizWeb'));
beforeEach(inject(function(_$rootScope_, _$controller_, _$q_, _WebsiteService_) {
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
$controller = _$controller_;
service = _WebsiteService_;
var websiteDeferred = _$q_.defer();
websiteDeferred.resolve(mockWebsite);
spyOn(service, 'load').andReturn(websiteDeferred.promise);
var blogsDeferred = _$q_.defer();
blogsDeferred.resolve(mockResource);
spyOn(mockWebsite, '$get').andReturn(blogsDeferred.promise);
var blogListDeferred = _$q_.defer();
blogListDeferred.resolve(mockBlogPostList);
spyOn(mockResource, '$get').andReturn(blogListDeferred.promise);
var authorDeferred = _$q_.defer();
authorDeferred.resolve(mockAuthor);
spyOn(mockBlogPostList[0], '$get').andReturn(authorDeferred.promise);
spyOn(mockBlogPostList[1], '$get').andReturn(authorDeferred.promise);
$controller('BlogListController',
{'$rootScope' : $rootScope, '$scope': $scope, 'WebsiteService': service});
$rootScope.$apply();
}));
it('should make Blog menu item active.', function() {
expect($rootScope.activeMenu.blog == 'active');
});
it('should have selectBlogPage() function.', function() {
expect($scope.selectBlogPage).toBeDefined();
});
describe('BlogListController setup(pageNumber) function', function() {
it('should have currentPage set to 1.', function() {
expect($scope.page.currentPage).toBe(1);
});
it('should have two blogs and first one is Test Blog.', function() {
expect($scope.blogs.length).toEqual(2);
expect($scope.blogs[0].title).toEqual('Test Blog');
});
it('should have first blog with title "Test Blog" and author "author"', function() {
expect($scope.blogs[0].title).toEqual('Test Blog');
expect($scope.blogs[0].author.displayName).toEqual('author');
});
it('should have second blog with title "Another Blog" and author "author"', function() {
expect($scope.blogs[1].title).toEqual('Another Blog');
expect($scope.blogs[1].author.displayName).toEqual('author');
});
});
});
The trick is to use AngularJS deferred object by calling $q.deferred()
, and use deferred APIresolve(value)
to return our mock resources. Then use Jasmine spyOn()
function to stub our resource function and return promise object associated with the deferred object. After we setup all the mock up resources along the chain of promises, and initialize BlogListController
, we have to do a final step $rootScope.$apply();
to trigger AngularJS $digest cycle programmatically, so our promises can be resolved.
Conclusion
Writing unit test is not easy. But it is so important, because writing unit test can actually make you think hard to write clean and elegant code.
With
behavior-driven development, we can describe our test cases in a more understandable way, and it helps better communication between developers and business. Business can tell user stories, and developer can translate those stories into unit test, so when requirements change, we can easily find out what part of code we need to change as well.
My experience with BDD and Jasmine is very pleasant.