Pages

Saturday, 6 April 2013

Building and using single page JavaScript applications is fun. It brings richness on client side without having to install any external plugins. While building these applications, we write a lot of JavaScript code to run on client side. Releasing such large amount of code to client without testing will be a sin. We might leave the code with some unnoticed bugs if we do not test.

If you haven’t followed earlier posts, take a look at the code on GitHub.

Jasmine and setting up
Jasmine is a BDD (Behaviour Driven Development) framework for testing JavsScript code. It doesn’t depend on any other JavaScript library. It has nice support to create spies (mocks), which helps to easily get rid of some concrete dependencies. Jasmine can be used to unit test anything written in JavaScript.

In order to run jasmine tests and view their results, we need to create an HTML page. Following references should be added to the page:

Jasmine core library

Jasmine HTML library

Jasmine style sheet

Source files of the script to be tested, which is also referred as System under test (SUT)

File containing unit tests for the above source

Any other library (like jQuery, jQuery UI, Angular) on which the source depends

As we will be testing code written using Angular JS, we should include angular-mocks.js library, which has mock services to replace some of the most commonly used services.

In body tag of the HTML page, a couple of statements are to be added to bootstrap Jasmine. Following is the template of a Jasmine spec runner page:

Dependencies of the controller are clearly visible from the signature. As we need to inspect behaviour of the controller in isolation, we must mock these services. Following is the signature of ShoppingCartController:

function ShoppingCartCtrl($scope, $window, shoppingData, shared) {
}

As these services will be used across specs, it is better to create them globally and initialize them in beforeEach block. Since we will not hit the actual service, we need to use some static data to make the job of testing AJAX calls easier.

The second beforeEach block will set-up all dependencies for the controller. So, it needs a number of services to perform its job. We will discuss their importance in shortly.

Resolving Dependencies
First and most important dependency of the controller is the $scope service. We need to create our own scope and pass it as a parameter while creating object of the controller. Using $rootScope, it is very easy to create our own scope:

ctrlScope = $rootScope.$new();

Second dependency is the $window service. As we are using href property of location alone, we can create a custom object with this property alone.

windowMock = { location: { href: ""} };

Third dependency shoppingData service is a wrapper to call backend data services. It used another service, $http to send AJAX requests. Angular JS team has made our job easy by creating $httpBackend, a mock for $http. $httpBackend provides a nice interface to send our own response when an AJAX request is made.

shoppingData service has three functions: getAllItems, addAnItem and removeItem. Let’s create spies for these functions as follows:

The function andCallThrough delegate calls to the actual implementations. The function getAllItems sends an HTTP GET request to the service. Following statement configures a custom response on $httpBackend when Angular detects any such request:

httpBackend.expectGET('/api/shoppingCart/').respond(storedItems);

Fourth and final dependency is the shared service. Following snippet creates a mock shared service with a spy for setCartItems, the only function used in ShoppingCartCtrl:

sharedMock = {
setCartItems: jasmine.createSpy('setCartItems')
};

Now that we have all the mocks ready, let’s create an object of the controller.

Testing behaviour of the controller
On creation of the controller, it calls getAllItems function of shoppingData service to fetch details of all items. The test for this behaviour should check if the right function is called and if it sets value to the items property. Following test shows this:

Upon calling addItem function of the controller, it calls addAnItem function of shoppingData service. As it makes an HTTP post request to the service, $httpBackend should be configured to respond when it finds a post request. Test looks as follows:

The function removeItem can also be tested in similar way. But, what if a request fails? The $errorMessage property of scope should be assigned with a friendly error message. A request can be forced to fail by passing a JavaScript object literal with an error status code to $httpBackend. Let’s see this in action:

mySortFunction is used to convert numeric value to number. We can test this function by passing a number in the form of a string and checking if it returned a number to us. We need to set the property sortExpression before calling the function.

The totalPrice function is very easy to test, as we need to just check if it sets some value to the returned variable.

On click of Purchase Items link on the page, the user has to be navigated to CheckoutItems view and setCartItems function of shared service should be called to pass the items array to the second view. As we are setting navigation URL to window.location.href, for which we created a mock, the test has to check if this property is set to some value. Following test verifies these functionalities:

Now view the test runner page on browser. All the tests should be passed. I encourage you to play a bit with the code and tests and check the result of test after the changes. This way, we will get to know more about Jasmine's behavior as well as unit testing.

You can download the code including unit tests from the following GitHub repo: AngularShoppingCart

About Me

I am a software developer working on and passionate about Microsoft Technologies and anything new in the Web world. Lately, spending a lot of time with JavaScript and finding it much deeper than what I knew for past few years. I enjoy writing code and writing about code. My contribution to community earned me a Microsoft MVP award (ASP.NET/IIS) and a DZone MVB award. When not in front of computers, like to spend time with melodious Hindi and Telugu tunes or watching Cricket