Unit testing with angularjs, requirejs, jquery, karma and jasmine.

19 July 2015




Unit testing with angular requirejs karma and jasmine

In this example we're going to create a sample test for the an application called "FarmApp", which should ressemble an imaginary farm. Here we are testing our AnimalListCtrl.js a simple angular JS controller that lists animals of our current farm app.

project directory tree

app/js$ tree
.
├── animations.js
├── app.js
├── controllers.js
├── directives.js
├── filters.js
├── main.js
├── routes.js
├── services.js
├── test-main.js
└── views
    └── animal_list
        ├── animal_list.html
        ├── animalList.js
        └── animalListSpec.js
//...


services.js

Our services that will interact with a backend resource:

define(['angular', 'angularResource'], function (angular) {
    'use strict';  
var services = angular.module('farmapp.services', ['ngResource']); services.factory('AnimalService', ['$resource', function($resource){
return $resource('/farmWebApp/rest/animal/:id', { id: "@id" }, { 'getByName': {method: 'GET', params: {name: '@name'}, url: '/farmWebApp/rest/animal/:name'} } ); }]); return services; });

Controllers

each controller, is layered in such a way that their corresponding test and html view are bundled together in the same folder:

app/js$ tree
.
└── views
    └── animal_list
        ├── animal_list.html
        ├── animalList.js
        └── animalListSpec.js
//...


animalList.js

Here is the code for the animal list controller, which will be responsible for fetching a list of animals from the service.

define(['angular', 'services'], function (angular) {
    'use strict';
    var animalList = angular.module('farmapp.animalList', ['farmapp.services']);
animalList.controller('AnimalListCtrl', ['$scope', '$filter', 'AnimalService', function($scope, $filter, AnimalService) { $scope.deleteAnimal = function (aId) { AnimalService.remove({ id: aId }); $scope.loadAnimalListTable(); };
$scope.loadAnimalListTable = function() { AnimalService.query(function(data) { $scope.animals = data; }); }; $scope.loadAnimalListTable(); }]);
return animalList; });

karma.conf.js

In order to trigger unit tests in our application we will use Karma along with Jasmine for JS Unit tests.

module.exports = function(config){
  config.set({
    basePath : './',
    frameworks: ['jasmine'],
    // test results reporter to use
    // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
    reporters: ['progress'],
    // 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,
    logLevel: config.LOG_DEBUG, 
// enable / disable watching file and executing tests whenever any file changes autoWatch: true, // If browser does not capture in given timeout [ms], kill it captureTimeout: 60000, browserNoActivityTimeout: 10000, // Continuous Integration mode // if true, it capture browsers, run tests and exit singleRun: true, plugins : [ 'karma-chrome-launcher', 'karma-firefox-launcher',
'karma-script-launcher', 'karma-phantomjs-launcher', 'karma-junit-reporter', 'karma-jasmine', 'karma-requirejs'
], files : [ 'node_modules/requirejs/require.js', 'node_modules/karma-requirejs/lib/adapter.js', 'app/js/test-main.js', {pattern: 'app/lib//*.js', included: false}, {pattern: 'app/js//.js', included: false}, {pattern: 'app/js/views/**/Spec.js', included: false} ], // list of files to exclude exclude: [ 'app/js/main.js' ], browsers: ['PhantomJS'] junitReporter : { outputFile: 'test_out/unit.xml', suite: 'unit' } }); };

test-main.js

test-main is where we define all requireJS config setup for tests.

var tests = [];
for (var file in window.karma.files) {
  if (window.karma.files.hasOwnProperty(file)) {
    if (/Spec.js$/.test(file)) {
      tests.push(file);
    }
  }
}

requirejs.config({ // Karma serves files from '/base' baseUrl: '/base/app/js', paths: { jquery: '../lib/bower_components/jquery/dist/jquery.min', angular: '../lib/bower_components/angular/angular.min', angularAnimate: '../lib/bower_components/angular-animate/angular-animate.min', angularCookies: '../lib/bower_components/angular-cookies/angular-cookies.min', angularResource: '../lib/bower_components/angular-resource/angular-resource.min', angularRoute: '../lib/bower_components/angular-route/angular-route.min', angularScenario: '../lib/bower_components/angular-scenario/angular-scenario', angularMocks: '../lib/bower_components/angular-mocks/angular-mocks', angularTouch: '../lib/bower_components/angular-touch/angular-touch',
angularSanitize: '../lib/bower_components/angular-sanitize/angular-sanitize.min', jqueryui: '../lib/bower_components/jquery-ui/jquery-ui.min', bootstrap: '../lib/bower_components/bootstrap/dist/js/bootstrap.min', angularBootstrap: '../lib/bower_components/angular-bootstrap/ui-bootstrap-tpls' },
shim: { 'angular': { 'deps': ['jquery'], 'exports': 'angular' }, 'angularResource': { 'deps': ['angular'], exports: 'angularResource' }, 'angularRoute':{ 'deps': ['angular'] }, 'angularAnimate': { 'deps': ['angular'] }, 'angularCookies':{ 'deps': ['angular'] },
'angularMocks': { deps:['angular'] }, 'angularTouch': { deps:['angular'] }, 'angularScenario': { deps:['angular'] }, 'angularSanitize': { deps:['angular'] }, 'angularBootstrap' : { deps: ['angular', 'bootstrap'] }, 'bootstrap' : {deps:['jquery']} },
priority: [ 'angular' ] // ask Require.js to load these files (all our tests) deps: tests, // start test run, once Require.js is done callback: window.karma.start });

animalListSpec.js

In the unit test, in this case animalListSpec.js, are all the tests that should be used by Karma for testing the animalList controller and view:

define(['angular', 'app', 'views/animal_list/animalList','angularMocks'],
    function(angular, app, animalList, angularMocks) {
    describe('AnimalListCtrl', function() {
     var rootScope,
         scope,
         controller,
         mockAnimalService;        
var dataJSON = [ {"id" : 1,"name" : "herbie","type" : "caprine","gender" : "M"}, {"id" : 2,"name" : "melvin","type" : "bovine","gender" : "M"}, {"id" : 3,"name" : "daisy","type" : "bovine","gender" : "F"}, {"id" : 4,"name" : "feather","type" : "avian","gender" : "F"}]; beforeEach(module('farmapp')); beforeEach(function() { mockAnimalService = { query: function(callback) { callback(dataJSON); }, remove : function(params) { } }; }); beforeEach(inject(function($rootScope, $injector, $controller) { rootScope = $rootScope; scope = $rootScope.$new(); controller = $controller('AnimalListCtrl', { $scope: scope, AnimalService: mockAnimalService }); })); it('should be defined and not null', function() { expect(controller).toBeDefined(); expect(controller).not.toBeNull(); }); it('should call loadAnimalList', function() { spyOn(mockAnimalService, 'query').and.callThrough(); scope.loadAnimalListTable(); expect(mockAnimalService.query).toHaveBeenCalled(); }); it('should call deleteAnimal', function() { spyOn(mockAnimalService, 'remove').and.callThrough(); spyOn(mockAnimalService, 'query').and.callThrough();
scope.deleteAnimal(); expect(mockAnimalService.remove).toHaveBeenCalled(); expect(mockAnimalService.query).toHaveBeenCalled(); }); }); });

Notice how mockAnimalService mocks calls to animalService, returning a fake animal list based on JSON test data. Also notice how here spyOn is used to actually monitor these calls and and.callThrough() gurantees the calls will be passed through, otherwise the calls to the mock functions wouldn't be done.

Once Unit test is created you can run tests by either building and testing with grunt, in the command line in folder where Gruntfile.js is located, or either by typing karma start where karma.conf.js file is located.



comments powered by Disqus