Using jasmine and karma to Write and Run Unit Tests for AngularJS Applications in Visual Studio

Dhananjay Kumar / Tuesday, July 12, 2016

 Note: Although we say this post will show you how to use Visual Studio to write AngularJS applications and unit tests, you can use these methods to configure test environments for any IDE.  

To write unit tests I will use jasmine, to run them I’ll use karma, and to create proxies of AngularJS components like filter, controller, and service, I’ll use ng-mock. In this post we will cover the following topics:

  • Setting up the development environment
  • Setting up the test environment for jasmine
  • Setting up the karma test runner environment
  • Writing the unit test for filter, controller, and service

We will use npm to install dependencies and Visual Studio to write code, and we’ll run tests using karma from the command prompt. At the end of the post, we should have tests running as shown in the image below:

So let’s get started!

Step 1

Create an ASP.NET Web Application project in Visual Studio by choosing the Empty template project type.

Even though I am creating an AngularJS project in Visual Studio, you can create project using any IDE of your choice. The only thing you need to keep in mind is that all commands we will run here should be executed inside the root folder of the project.

Step 2

Once the project is created, launch the command prompt as an admin and change the directory to the root folder of created project folder. In the case of Visual Studio, change the directory to the project folder, not the solution folder.

 

For example, the folder structure I am using is as follows:

  • AngularJSUnitTestDemo : Solution Folder
  • AngularJSUnitTestDemo : Root folder of project. So, we navigated here.

 Step 3

We need to make sure that NodeJS is installed on the machine. If it is not installed, download and install it from https://nodejs.org/en/. We can verify whether NodeJS is installed or not by running the following command:

  • node --version 

If we get NodeJS version as output, then NodeJS is installed on the machine.

In this step, we installed and verified NodeJS.

 

Step 4

Next, let’s install AngularJS to the project. There are various ways to do so (NuGet package, Bower etc.), however, I am choosing NPM to install AngularJS in the project. To install, run the npm command as shown below:

  •  npm install angular --save

In this step, we have installed AngularJS to the project.

Step 5

Now we are going to use Karma test runner to run tests.  Learn more about Karma here: https://karma-runner.github.io/1.0/index.html Created by the Angular team, Karma is a spec-based test runner. To install Karma, run the command as shown below:

  •  npm install -g karma --save-dev

In this step, we have installed Karma in the project.

Step 6

We are going to use Jasmine, behavior driven JavaScript Test framework to write Unit Tests. Learn more about Jasmine here:  http://jasmine.github.io

We will install Jasmine Core and Jasmine Karma. To install them run the command as shown below:

  • npm install karma-jasmine jasmine-core --save-dev

 In this step, we have installed the jasmine core and the jasmine karma plugin in the project.

Step 7

We will use the ngMock module to mock up AngularJS services, modules, controllers, filters and more. To use ngMock, we need to install the angular-mocks library in the project. To do so, run the command as shown below:

  • npm install angular-mocks --save-dev

 In this step, we have installed angular-mocks library in the project.

 Step 8

Karma allows us to run tests in multiple browsers. To do so, we need to install different browser plugins. In this example, I’d like to run a test in Chrome, so I’ll go ahead and install a Chrome browser plugin as follows:

  • npm install karma-chrome-launcher --save-dev

In this step, we have installed the Chrome browser karma launcher. If you wish, you can install other browser launchers too.

Step 9

We can have any folder structure for the project adhering to our business requirements. However, for the purpose of this post, I am going to keep it simple. I will create two folders: app and tests. The app folder will keep all Angular scripts files and the tests folder to keep all the tests. To create these folders,  run the commands as shown below:

  • md app
  • md tests

 Step 10

This step is only required if you are working in Visual Studio, otherwise you can skip this step. We are going to add all newly installed files and newly created folder in the project. Go back to Visual Studio.

In this step we have included all of our newly created files and folders in Visual Studio project.

 Step 11

In this step, we are going to add new files to the project:

  • CalculatorApp.js to write AngularJS code. Add this file in app folder.
  • CalculatorAppTest.js to write unit tests for controller, filters, services written in CalculatorApp.js. Add this file in tests folder.
  • SpecRunner.html to display test results in browser using jasmine
  • Index.html, html page of the application.

To add files in Visual Studio,

  • Right click on the project/ app folder and add a file called CalculatorApp.js
  • Right click on the project/tests folder and add a file called CalculatorAppTest.js
  • Right click on the project and add an HTML file called SpecRunner.html
  • Right click on the project and add an HTML file called index.html

In this step, we have added new files to write code, unit test and display test results.

Step 12

In this step, we will setup SpecRunner.html. This will render test results in the browser. So, open SpecRunner.html and add the following references.



Jasmine Test Results
	<meta charset="utf-8" />
    <link href="node_modules/jasmine-core/lib/jasmine-core/jasmine.css" rel="stylesheet" />
    <s cript src="node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></s cript>
    <s cript src="node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></s cript>
    <s cript src="node_modules/jasmine-core/lib/jasmine-core/boot.js"></s cript>

    <s cript src="node_modules/angular/angular.js"></s cript>
    <s cript src="node_modules/angular-mocks/angular-mocks.js"></s cript>

    <s cript src="app/CalculatorApp.js">
    <s cript src="tests/CalculatorAppTest.js"></s cript>

Step 13 - Writing Tests for AngularJS filter

Finally, we have reached to step in which we will write some tests! Let’s start by writing tests for the AngularJS filter. Add following test to CalculatorAppTest.js.

describe('Calculator App Tests', function () {

    beforeEach(module('MyApp'));

    describe('reversestringfiltertest', function () {

        var reverse;
        beforeEach(inject(function ($filter) { //initialize filter
            reverse = $filter('reverse', {});
        }));

        it('Should reverse a string', function () { 
            expect(reverse('india')).toBe('aidni'); //pass test
            expect(reverse('don')).toBe('don'); //fail test
        });

    });


});

In the above code snippet, we are creating a proxy of the module and filter using ng-mock. We have written tests for the filter name ‘reverse’ from module ‘MyApp’ and we are injecting the filter using the $filter service of ng-mock.

At this point, if we go ahead and run the SpecRunner.html, we will get a failed test result because we have not created the AngularJS filter or the ‘MyApp’ module.

So now let us go ahead and create an AngularJS Module ‘MyApp’ with filter ‘reverse’. We will create these in the App/CalculatorApp.js

var MyApp = angular.module("MyApp", []);

MyApp.filter('reverse', [function () {
    return function (string) {
        return string.split('').reverse().join('');
    }
}]);

Now go back and run once again, run SpecRunner.html. We will find, once again test is failed. As it is showing in test failure message that – Expected ‘nod’ to be ‘don’

 If you remember, we deliberately wrote a failed test. So now let’s go ahead and fix the test in CalculatorAppTest.js

describe('Calculator App Tests', function () {

    beforeEach(module('MyApp'));

    describe('reversestringfiltertest', function () {

        var reverse;
        beforeEach(inject(function ($filter) { //initialize filter
            reverse = $filter('reverse', {});
        }));

        it('Should reverse a string', function () { 
            expect(reverse('india')).toBe('aidni');
            expect(reverse('don')).toBe('nod'); 
        });

    });


});

When we run the test in SpecRunner.html, we’ll find all tests are passed as shown in the image below.

In this step, we wrote a test for AngularJS filter and ran that using Jasmine SpecRunner.html.

Problems in running test using SpecRunner.html

You may have noticed the only problem in the approach above is that each time we change the test code or source code, we need to go back and either load SpecRunner.html again or refresh it manually to see the updated test result. In real project development, this could be a pain. We need a mechanism in which whenever the code changes, tests should be executed automatically. To do this, we have test runner. We have already installed the Karma test runner to automatically run tests whenever code changes, so now let’s configure it to do that.

Step 14

To configure Karma, we need to run command Karma init.

  • karma init

You will be prompted with many questions to answer, use the following answers as displayed below:

Karma.cnf.js file has been created after answering those questions. Next, let us open Visual Studio and include the newly created file Karma.conf.js in project.  Open Karma.conf.js and add references of Angular and Angular Mock. Your Files sections should look like the image below:

files: [
      'node_modules/angular/angular.js',
      'node_modules/angular-mocks/angular-mocks.js',
      'app/*.js',
      'tests/*Test.js'
    ],

Next go back to command prompt and run the command

  •  karma start

As soon as we run the karma start command, tests will start executing and we can see test results on the command prompt as shown in the image below.  As you change the code and save the file, the test will be executed automatically.

Step 15

Now let’s write a unit test for the AngularJS Controller. Add the following test to CalculatorAppTest.js and add a test for the controller below to the filter test or below the filter test describe.

describe('addcontrollertest', function () {
        var $controller;
        beforeEach(inject(function (_$controller_) {
            $controller = _$controller_;
        }));
        it('1 + 1 should equal 2', function () {
            var $scope = {};
            var controller = $controller('CalculatorController', { $scope: $scope });
            $scope.num1 = 1;
            $scope.num2 = 2;
            $scope.add();
            expect($scope.sum).toBe(3);
        });
    });

We are creating a proxy of the controller and injecting that using ng-mock $controller service.  Once the controller is injected, take the reference of CalculatorController (controller in the test here) and calling the add method.  At this point the test will fail, since we have not written controller yet.  Karma is giving the failure message that CalculatorController is not a function.

Next let us go ahead and write the controller in the ‘MyApp’ module.

MyApp.controller('CalculatorController', function ($scope) {

    $scope.add = function () {
        $scope.sum = $scope.num1 + $scope.num2;
    }
});

As soon as we save the CalculatorApp.js file after writing the controller, the test will pass as shown in the image below:

Step 16: Testing AngularJS Factory with local data  

Let us say that we have created an AngularJS service using factory method name PlayerLocalApi. This service is returning local data which is hard coded as of now. The Service is created as shown in the listing below:

MyApp.factory('PlayerLocalApi', function () {
    //var data = [{ "Name": "Dhananjay Kumar", "Age": 33.0 }];
    var data = [{ "Id": "1", "Name": "Dhananjay Kumar", "Age": 33.0 }, { "Id": "2", "Name": "Sachin Tendulkar", "Age": 22.0 }, { "Id": "6", "Name": "rahul dravid", "Age": 60.0 }];
    var PlayerLocalApi = {};
    PlayerLocalApi.getPlayers = function () {
        return data;
    }

    return PlayerLocalApi;
});

We can write a unit test to test the service as shown in the listing below:

describe('playerservicetest', function () {
        var data = [{ "Id": "1", "Name": "Dhananjay Kumar", "Age": 33.0 }, { "Id": "2", "Name": "Sachin Tendulkar", "Age": 22.0 }, { "Id": "6", "Name": "rahul dravid", "Age": 60.0 }];
        var PlayerLocalApi = {};

        beforeEach(inject(function (_PlayerLocalApi_) {
            PlayerLocalApi = _PlayerLocalApi_;
        }));
        it('should return search player data', function () {
            expect(PlayerLocalApi.getPlayers()).toEqual(data);

        });

    });

In the above code snippet, we are injecting the service PlayerLocalApi and then calling getPlayers on that. Also, we have exact test data to test the data returns from the service. This test will pass.

Just to make sure that we have written the test correctly, go back to service and comment the second data and uncomment the first data so the modified service is as listed below:

MyApp.factory('PlayerLocalApi', function () {
    var data = [{ "Name": "Dhananjay Kumar", "Age": 33.0 }];
   //var data = [{ "Id": "1", "Name": "Dhananjay Kumar", "Age": 33.0 }, { "Id": "2", "Name": "Sachin Tendulkar", "Age": 22.0 }, { "Id": "6", "Name": "rahul dravid", "Age": 60.0 }];
    var PlayerLocalApi = {};
    PlayerLocalApi.getPlayers = function () {
        return data;
    }

    return PlayerLocalApi;
});

Now the service is returning data which is not the same as test data in the test so we will get a failed test as shown in the image below:

And that’s how to write unit tests for services returning local data.

Conclusion

In this post we covered a lot! We learned how to:

  • Set up a development environment
  • Set up a test environment for jasmine
  • Setup a karma test runner environment
  • Write unit tests for filter, controller, and service

In further posts, we will see how to write unit tests for $http, $q services etc. I hope you find this post useful. Thanks for reading!