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:
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>
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:
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:
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) { }
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 } });
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();
windowMock = { location: { href: ""} };
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);
httpBackend.expectGET('/api/shoppingCart/').respond(storedItems);
sharedMock = injector.get('shared'); sinon.spy(sharedMock, 'setCartItems');
ctrl = injector.get('$controller')(ShoppingCartCtrl, { $scope: ctrlScope, $window: windowMock, shoppingData: _shoppingData, shared: sharedMock });
teardown: function () { sharedMock.setCartItems.restore(); _shoppingData.getAllItems.restore(); _shoppingData.addAnItem.restore(); _shoppingData.removeItem.restore(); }
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"); });
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"); });
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"); });
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"); });
test("Should calculate totalPrice", function () { ctrlScope.items = storedItems; notEqual(ctrlScope.totalPrice(), 0, "Total price is calculated"); });
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"); });
You can download the code including unit tests from the following GitHub repo: AngularShoppingCart
Happy coding!
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4