Angular JS is known for and is becoming increasingly popular due to its nature of testability. Angular’s in-the- box abstractions make any amount of code easily testable. Code written using Angular JS can be tested using any JavaScript unit testing framework out there, may it be QUnit, Jasmine, Mocha or some other library.
In an older post, we wrote unit tests for the AngularShoppingCart application using Jasmine. In this post, we will discuss on unit testing Angular JS’ controller using QUnit and Sinon.
If you haven’t followed earlier posts, take a look at the code on GitHub.
QUnit, Sinon and setting up
QUnit is a JavaScript unit testing framework developed by jQuery team. It is used in the projects like jQuery, jQuery UI, jQUery Mobile. QUnit is a very simple and generic framework that can be used to test any piece of JavaScript code. Unlike Jasmine, QUnit doesn’t have built-in support for creating spies.
Sinon is a JavaScript library that makes the process of creating spies, mocks and stubs easier. It doesn’t depend on any other JavaScript library and easily integrates with any JavaScript unit test framework. Official site has a nice API documentation covering different features of the library.
QUnit tests run on an HTML page. We need to add references to following files in the page:
- QUnit library
- QUnit style sheet
- Sinon library
- Source files of the scripts to be tested
- File containing unit tests for 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.
Following is the HTML page to run QUnit tests:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>QUnit Test Runner</title> <!-- QUnit stylesheet --> <link href="../../Styles/qunit-1.11.0.css" rel="stylesheet" type="text/css" /> <!-- QUnit for testing and Sinon for test spies, mocks and stubs --> <script src="../../Scripts/qunit-1.11.0.js" type="text/javascript"></script> <script src="../../Scripts/sinon-1.7.3.js" type="text/javascript"></script> <!-- JavaScript libraries on which source depends --> <script src="../../Scripts/angular.js" type="text/javascript"></script> <script src="../../Scripts/angular-mocks.js" type="text/javascript"></script> <!-- Script source to test and other files on which source depends --> <script src="../../Scripts/app/ShoppingModule.03.js" type="text/javascript"></script> <script src="../../Scripts/app/ShoppingCartController.03.js" type="text/javascript"></script> <script src="../../Scripts/app/CartCheckoutController.js" type="text/javascript"></script> <!-- Test Script --> <script type="text/javascript" src="ShoppingCartControllerSpec.js"></script> <!--<script src="CartCheckoutTestSpec.js" type="text/javascript"></script>--> </head> <body> <div id="qunit"> </div> <div id="qunit-fixture"> </div> </body> </html>
Specs and Suits
QUnit has a set of blocks to create modules, setup required resources, clear them after running the tests, create a test suit and a number of assertions. Following are the blocks that we will be using:
- module: Defines a module, which can be used to group a number of related tests. Two functions namely, setup and teardown can be added to the module to instantiate and clear the resources for unit tests
- test: Used to define a test spec. Accepts a name and function containing logic to be executed. The logic contains assertions to evaluate the behaviour
- ok: A boolean assertion, that passes when the argument returns a Boolean true
- equal: An assertion that compares two values and passes if they are equal. We also have notEqual assertion that passes if the values are not equal
QUnit’s API documentation page contains details of all available blocks and asserts.
Sinon Spies and Stubs
Sinon is a very rich library with a huge API and a lot of features. We need very few of them in our specs:
- spy: A spy can be an anonymous function or a wrap around an existing function. There are a number of ways to create and check if the spy is invoked. We will be using one of the approaches.
sinon.spy(obj,”methodName”) – creates a spy for a method with specified name
obj.methodName.called – to check if the method is called
obj.methodName.restore() – to restore the original functionality of the method
- stub: Stubs are spied with existing behaviour. They should be used when we want a function to behave in a way we want. We will be using the following syntax to create a stub in the unit test that we are going to write in a while:
sinon.stub(obj,”methodName”, functionName) – Replaces the function with passed name with the logic of an existing function
Let’s start testing the functions defined in ShoppingCartController.
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 setup 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.
var shoppingCartStaticData = [ { "ID": 1, "Name": "Item1", "Price": 100, "Quantity": 5 }, { "ID": 2, "Name": "Item2", "Price": 55, "Quantity": 10 }, { "ID": 3, "Name": "Item3", "Price": 60, "Quantity": 20 }, {"ID": 4, "Name": "Item4", "Price": 65, "Quantity": 8 } ]; //Mocks var windowMock, httpBackend, _shoppingData, sharedMock; //Injector var injector; //Controller var ctrl; //Scope var ctrlScope; //Data var storedItems;
We need to create a module to initialize all of the above objects and clear them. Following is the skeleton of the module:
module(“Shopping module”, { setup: function(){ //Initialize all above objects }, teardown: function(){ //Clear up objects and restore spies } });
Resolving Dependencies
Unlike Jasmine, in QUnit tests we need to use injector to get the dependencies resolved. They aren’t resolved automatically. Following statement gets an instance of the injector:
injector = angular.injector(['ng', 'shopping', 'appMocks']);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 = injector.get('$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. In QUnit tests, a decorator statement must be added to get the object of $httpBackend. Following snippet shows it:
var appMocks = angular.module("appMocks", []); appMocks.config(function ($provide) { $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); });shoppingData service has three functions: getAllItems, addAnItem and removeItem. We need to create stubs for these functions with a call to the original function. The stubs will be used to inspect if the function is called. Following snippet demonstrates it:
httpBackend = injector.get('$httpBackend'); _shoppingData = injector.get('shoppingData'); sinon.stub(_shoppingData, "getAllItems", _shoppingData.getAllItems); sinon.stub(_shoppingData, "addAnItem", _shoppingData.addAnItem); sinon.stub(_shoppingData, "removeItem", _shoppingData.removeItem);
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 = injector.get('shared'); sinon.spy(sharedMock, 'setCartItems');
Now that we have all the mocks ready, let’s create an object of the controller.
ctrl = injector.get('$controller')(ShoppingCartCtrl, { $scope: ctrlScope, $window: windowMock, shoppingData: _shoppingData, shared: sharedMock });
All spies have to be restored in the teardown block as shown below:
teardown: function () { sharedMock.setCartItems.restore(); _shoppingData.getAllItems.restore(); _shoppingData.addAnItem.restore(); _shoppingData.removeItem.restore(); }
Testing behaviour of the controller
Note: If you have already read the post on Jasmine, you may skip rest of the post and check the code as most of the explanation remains same
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:
test("Should call getAllItems function on creation of controller", function () { ok(_shoppingData.getAllItems.called, "getAllItems is called"); httpBackend.flush(); notEqual(storedItems.length, 0, "Number of items loaded is not 0"); });
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:
test("Should call addAnItem function of the shoppingData service", function () { httpBackend.expectPOST('/api/shoppingCart/', {}).respond(storedItems.push({ "Id": 5, "Name": "Item5", "Price": 70, "Quantity": 10 })); ctrlScope.addItem({}); ok(_shoppingData.addAnItem.called, "addAnItem function is called"); httpBackend.flush(); equal(storedItems.length, 5, "New item is added to the list"); });
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:
test("Should assign an error message", function () { httpBackend.expectDELETE('/api/shoppingCart/1').respond({ status: 500 }); ctrlScope.removeItem(1); notEqual(ctrlScope.errorMessage,"","An error message is set to the variable in scope"); });
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.
test("Should return a number when a number is passed in", function () { var item = { "Number": "123" }; ctrlScope.sortExpression = "Number"; var numVal = ctrlScope.mySortFunction(item); equal(typeof numVal, "number", "Value returned is a number"); });
The totalPrice function is very easy to test, as we need to just check if it sets some value to the returned variable.
test("Should calculate totalPrice", function () { ctrlScope.items = storedItems; notEqual(ctrlScope.totalPrice(), 0, "Total price is calculated"); });
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:
test("Should set value in shared and value of href set", function () { ctrlScope.items = storedItems; ctrlScope.purchase(); ok(sharedMock.setCartItems.called, "setCartItems function is called"); notEqual(windowMock.location.href,"","URL of the page is modified"); });
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 using QUnit, Sinon with Angular JS and also about unit testing.
You can download the code including unit tests from the following GitHub repo: AngularShoppingCart
Happy coding!
No comments:
Post a Comment
Note: only a member of this blog may post a comment.