Right from the first post on Angular JS, I have mentioned several times that unit testability is one of its primary design goals. Tutorial series on Angular’s website demonstrates end-to-end testing using Jasmine’s BDD style. I cannot have sound sleep unless I write some tests for the AngularShoppingCart application. In this post, we will see how a controller can be unit tested using Jasmine.
Note: If you are looking for a tutorial on unit testing using QUnit, I have a blog post on it: Unit Testing Angular JS Controller Using QUnit and Sinon
If you haven’t followed earlier posts, take a look at the code on GitHub.
Jasmine and setting up
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:
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:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <title>Jasmine Test Runner</title> <!-- Jasmine stylesheet --> <link rel="stylesheet" type="text/css" href="../../Styles/Jasmine.css"> <!-- Jasmine core and HTML --> <script type="text/javascript" src="../../Scripts/jasmine.js"></script> <script type="text/javascript" src="../../Scripts/jasmine-html.js"></script> <script type="text/javascript" src="../../Scripts/jasmine.async.js"></script> <!-- JavaScript libraries on which source depends --> <script type="text/javascript" src="../../Scripts/angular.js"></script> <script type="text/javascript" src="../../Scripts/angular-mocks.js"></script> <!-- Script source to test and other files on which source depends --> <script type="text/javascript" src="../../Scripts/app/ShoppingModule.03.js"></script> <script type="text/javascript" src="../../Scripts/app/ShoppingCartController.03.js"></script> <script type="text/javascript" src="../../Scripts/app/CartCheckoutController.js"></script> <!-- Test script --> <script type="text/javascript" src="ShoppingCartControllerSpec.js"></script> <!--<script type="text/javascript" src="CartCheckoutTestSpec.js"></script>--> </head> <body> <script type="text/javascript"> jasmine.getEnv().addReporter(new jasmine.TrivialReporter()); jasmine.getEnv().execute(); </script> </body> </html>
Unit testing 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 } ]; describe("ShoppingCartCtrl", function () { //Mocks var windowMock, httpBackend, _shoppingData, sharedMock; //Controller var ctrl; //Scope var ctrlScope; //Data var storedItems; //Loading shopping module beforeEach(function () { module("shopping"); }); beforeEach(inject(function ($rootScope, $httpBackend, $controller, shoppingData) { //Mock the services here } });
Resolving Dependencies
ctrlScope = $rootScope.$new();
windowMock = { location: { href: ""} };
shoppingData service has three functions: getAllItems, addAnItem and removeItem. Let’s create spies for these functions as follows:
httpBackend = $httpBackend; _shoppingData = shoppingData; spyOn(shoppingData, 'getAllItems').andCallThrough(); spyOn(shoppingData, 'addAnItem').andCallThrough(); spyOn(shoppingData, 'removeItem').andCallThrough();
httpBackend.expectGET('/api/shoppingCart/').respond(storedItems);
sharedMock = { setCartItems: jasmine.createSpy('setCartItems') };
ctrl = $controller(ShoppingCartCtrl, { $scope: ctrlScope, $window: windowMock, shoppingData: _shoppingData, shared: sharedMock });
it("Should call getAllItems function on creation of controller and set items property", function () { expect(_shoppingData.getAllItems).toHaveBeenCalled(); httpBackend.flush(); expect(ctrlScope.items.length).not.toBe(0); });
it("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({}); expect(_shoppingData.addAnItem).toHaveBeenCalled(); httpBackend.flush(); expect(storedItems.length).toBe(5); });
it("Should assign an error message", function () { httpBackend.expectDELETE('/api/shoppingCart/1').respond({ status: 500 }); ctrlScope.removeItem(1); expect(ctrlScope.errorMessage).not.toBe(""); });
it("Should return a number when a number is passed in", function () { var item = { "Number": "123" }; ctrlScope.sortExpression = "Number"; var numVal = ctrlScope.mySortFunction(item); expect(typeof numVal).toBe("number"); });
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:
it("Should set value in shared and value of href set", function () { ctrlScope.items = storedItems; ctrlScope.purchase(); expect(sharedMock.setCartItems).toHaveBeenCalled(); expect(windowMock.location.href).not.toBe(""); });
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