└── readme.md /readme.md: -------------------------------------------------------------------------------- 1 | # AngularJS Testing Cheat Sheet 2 | 3 | ### Contents 4 | 5 | [Unit testing routes](#routes) 6 | [Unit testing controllers](#controllers) 7 | 8 | 9 | 10 | ### Unit testing routes 11 | 12 | #### URLProvider 13 | 14 | Let's assume we have this routing setup: 15 | 16 | ```js 17 | angular.module('myApp', ['ngRoute']) 18 | 19 | .config(['$routeProvider', function($routeProvider) { 20 | $routeProvider 21 | .when('/', { 22 | 'templateUrl' : 'templates/main.html', 23 | 'controller' : 'HomeController' 24 | }) 25 | .when('/login', { 26 | 'templateUrl' : 'templates/login.html', 27 | 'controller' : 'LoginController' 28 | }) 29 | .otherwise({ 30 | 'redirectTo' : '/' 31 | }); 32 | }]); 33 | ``` 34 | 35 | In order to set up our unit tests so that they can test our routing code, we need to do a few things: 36 | 37 | - Inject the **$route**, **$location** and **$rootScope** services 38 | - Set up a mock back end to handle XHR fetching template code 39 | - Set a location and run a $digest lifecycle 40 | 41 | ```js 42 | describe('Routes test', function() { 43 | // Mock our module in our tests 44 | beforeEach(module('myApp')); 45 | 46 | // We want to store a copy of the three services we'll use in our tests 47 | // so we can later reference these services in our tests. 48 | var $location, $route, $rootScope; 49 | 50 | // We added _ in our dependencies to avoid conflicting with our variables. 51 | // Angularjs will strip out the _ and inject the dependencies. 52 | beforeEach(inject(function(_$location_, _$route_, _$rootScope_){ 53 | $location = _$location_; 54 | $route = _$route_; 55 | $rootScope = _$rootScope_; 56 | })); 57 | 58 | // We need to setup a mock backend to handle the fetching of templates from the 'templateUrl'. 59 | beforeEach(inject(function($httpBackend){ 60 | $httpBackend.expectGET('templates/main.html').respond(200, 'main HTML'); 61 | // or we can use $templateCache service to store the template. 62 | // $routeProvider will search for the template in the $templateCache first 63 | // before fetching it using http 64 | // $templateCache.put('templates/main.html', 'main HTML'); 65 | })); 66 | 67 | // Our test code is set up. We can now start writing the tests. 68 | 69 | // When a user navigates to the index page, they are shown the index page with the proper 70 | // controller 71 | it('should load the index page on successful load of /', function(){ 72 | expect($location.path()).toBe(''); 73 | 74 | $location.path('/'); 75 | 76 | // The router works with the digest lifecycle, wherein after the location is set, 77 | // it takes a single digest loop cycle to process the route, 78 | // transform the page content, and finish the routing. 79 | // In order to trigger the location request, we’ll run a digest cycle (on the $rootScope) 80 | // and check that the controller is as expected. 81 | $rootScope.$digest(); 82 | 83 | expect($location.path()).toBe( '/' ); 84 | expect($route.current.controller).toBe('HomeController'); 85 | }); 86 | 87 | it('should redirect to the index path on non-existent route', function(){ 88 | expect($location.path()).toBe(''); 89 | 90 | $location.path('/a/non-existent/route'); 91 | 92 | $rootScope.$digest(); 93 | 94 | expect($location.path()).toBe( '/' ); 95 | expect($route.current.controller).toBe('HomeController'); 96 | }); 97 | }); 98 | ``` 99 | 100 | **_resolves_** 101 | 102 | Resolves are a way of executing and finishing asynchronous tasks before a particular route is loaded. 103 | This is a great way to check if user is logged in, has authorizations and permissions, and even pre-load 104 | some data before a controller and route are loaded into the view. 105 | 106 | Consider this routing configuration where we add a resolve which returns a **promise**: 107 | 108 | ```js 109 | angular.module('myApp', ['ngRoute']) 110 | 111 | .config(['$routeProvider', function($routeProvider) { 112 | $routeProvider 113 | .when('/', { 114 | 'templateUrl' : 'templates/main.html', 115 | 'controller' : 'HomeController', 116 | 'resolve' : { 117 | 'allowAccess' : ['$http', function($http){ 118 | return $http.get('/api/has_access'); 119 | }] 120 | } 121 | } 122 | }) 123 | .otherwise({ 124 | 'redirectTo' : '/' 125 | }); 126 | }]); 127 | ``` 128 | 129 | In our test, we mock out each resolves dependencies and spy on each method using jasmine.createSpy() 130 | if you care if the method has been invoked. 131 | 132 | ```js 133 | describe('Routes test with resolves', function() { 134 | var httpMock = {}; 135 | 136 | beforeEach(module('myApp', function($provide){ 137 | $provide('$http', httpMock); 138 | })); 139 | 140 | var $location, $route, $rootScope; 141 | 142 | beforeEach(inject(function(_$location_, _$route_, _$rootScope_, $httpBackend, $templateCache){ 143 | $location = _$location_; 144 | $route = _$route_; 145 | $rootScope = _$rootScope_; 146 | 147 | $templateCache.put('templates/main.html', 'main HTML'); 148 | 149 | httpMock.get = jasmine.createSpy('spy').and.returnValue('test'); 150 | })); 151 | 152 | it('should load the index page on successful load of /', 153 | inject(function($injector){ 154 | expect($location.path()).toBe( '' ); 155 | $location.path('/'); 156 | 157 | $rootScope.$digest(); 158 | 159 | expect($location.path()).toBe( '/' ); 160 | expect($route.current.controller).toBe('HomeController'); 161 | 162 | // We need to do $injector.invoke to resolve dependencies 163 | expect($injector.invoke($route.current.resolve.allowAccess)).toBe('test'); 164 | })); 165 | }); 166 | ``` 167 | 168 | 169 | ### Unit testing controllers 170 | 171 | The controller is where we handle updating the views in our application. In setting up unit tests for controllers, 172 | we need to make sure: 173 | 174 | 1. Set up our tests to mock the module. 175 | 2. Store an instance of the controller with an instance of the known scope. 176 | 3. Test our expectations against the scope. 177 | 178 | > **_Note:_** 179 | > Testing **_controllerAs_** syntax is different than controllers that use $scope. 180 | > Here we're testing controllers that attaches data and method in the $scope object. 181 | 182 | Consider this simplistic controller 183 | 184 | ```js 185 | angular.module('myApp', []) 186 | .controller('ListCtrl', ['$scope', function($scope){ 187 | $scope.items = [ 188 | {'id': 1, 'label': 'First', 'done': true}, 189 | {'id': 2, 'label': 'Second', 'done': false} 190 | ]; 191 | 192 | $scope.getDoneClass = function(item) { 193 | return { 194 | 'finished': item.done, 195 | 'unfinished': !item.done 196 | }; 197 | } 198 | }]); 199 | ``` 200 | 201 | All it does is assign an array to its instance (to make it available to the HTML), and then has a function to figure out the presentation logic, which returns the classes to apply based on the item’s done state. Given this controller, let us take a look at how a Jasmine spec for this might look like: 202 | 203 | ```js 204 | describe('ListCtrl', function(){ 205 | // Mock the module myApp 206 | beforeEach(module('myApp')); 207 | 208 | var scope; 209 | 210 | beforeEach(inject(function($controller, $rootScope){ 211 | // To instantiate a new controller instance, we need to create 212 | // a new instance of a scope from the $rootScope with the $new() method. 213 | // This new instance will set up the scope inheritance that Angular uses at run time. 214 | // With this scope, we can instantiate a new controller and 215 | // pass the scope in as the $scope of the controller. 216 | scope = $rootScope.$new(); 217 | 218 | // Create the new instance of the controller 219 | $controller('ListCtrl', {'$scope' : scope}); 220 | })); 221 | 222 | // We'll test the two features of our controller 223 | // 1. The items data is populated on load 224 | // 2. The 'getDoneClass' method returns object based on item argument 225 | 226 | it('should have items available on load', function() { 227 | expect(scope.items).toEqual([ 228 | {'id': 1, 'label': 'First', 'done': true}, 229 | {'id': 2, 'label': 'Second', 'done': false} 230 | ]); 231 | }); 232 | 233 | it('should have highlight items based on state', function(){ 234 | var item = {'id': 1, 'label': 'First', 'done': true}; 235 | 236 | var actualClass = scope.getDoneClass(item); 237 | expect(actualClass.finished).toBeTruthy(); 238 | expect(actualClass.unfinished).toBeFalsy(); 239 | 240 | item.done = false; 241 | actualClass = scope.getDoneClass(item); 242 | expect(actualClass.finished).toBeFalsy(); 243 | expect(actualClass.unfinished).toBeTruthy(); 244 | }); 245 | 246 | }); 247 | ``` 248 | 249 | --------------------------------------------------------------------------------