└── 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 |
--------------------------------------------------------------------------------