61 |
62 | Angular Unit Testing Quick Start
63 |Introduction
64 |Why are you doing this?
65 |Angular was written from the ground up to be testable and yet there are scores of Angular developers who are not writing enough (if any) tests for their application. Why is this? I believe that it is because that while testing Angular is easy, 66 | actually getting to that first step is hard. I remember the feeling of despair I felt the first time I was tasked with writing tests for a project I was on. Where do I start? How do I get this Karma thing to actually run? Find my files? How 67 | do I instantiate my controller? What if it has a service dependency? Aaaaaaaaargh!
68 |I have since covered a lot of ground over the last couple years and I found that once you understand a few basic rules of engagement that testing in Angular is actually pretty straight forward and formulaic.
69 |I wanted to try to illustrate some of these basic patterns in an easy to read, approachable way so that developers can get to that first test without losing their mind
70 |Please use this material as a bridge to making tests a natural part of your development process; and if you find something amiss, do not hesitate to create a pull request on the 71 | repo. Nothing would make me happier! Enjoy!
72 | 73 |The Sample Project
74 |Where can I see the actual tests?
75 |The companion repository for this quick start guide can be found here https://github.com/simpulton/angular-testing-quick-start
76 | 77 |Unit Testing vs E2E
78 |How does Karma differ from Protractor?
79 | 80 |I like to split my tests up into three different categories:
81 | 82 |-
83 |
- 84 | End-to-End Tests - These are the tests where you want to mimic an actual user that visits your website. Each test contains a series of simulated user events (ex. go to http://mysite.com/home and then click on the button with ID 85 | 'my-button') and expected results (ex. after 200ms a new window should appear that says "Thank You"). 86 |
- 87 | Integration Tests - These tests will call directly into your code. For example, you can use an integration test to call an Angular service. Typically each test will focus on one function. The test calls the target function with 88 | a set of parameters and then checks to make sure the results match expected values. 89 |
- 90 | Unit Tests - These are the same as integration tests except you take extra steps to ensure that nothing is executed besides the one function you are testing. For example, when you test a service that uses $http to call a back end 91 | API, an integration test would include the API call. A unit test, however, would use a utility we will discuss later called $httpBackend to replace $http so that the code executed by the test is restricted to just the target function. 92 |
Protractor is the tool you will use for end-to-end tests while Karma handles integration and unit testing.
95 | 96 |Installing Karma
97 |How do I install Karma?
98 |
99 | npm install -g karma
100 |
101 |
102 |
103 | Configuring Karma
104 |How do does Karma know which files to include?
105 |
106 | karma init karma.conf.js
107 |
108 |
109 | After installing Karma, we need to set up the karma.conf file. The files array in karma.conf contains all the files needed to run a test. This includes:
110 | 111 |-
112 |
- Any angular libraries used by your code 113 |
- All your custom code 114 |
- All your test specs 115 |
Below is an example of a full karma.conf file.
118 | 119 | 120 |
121 |
122 |
123 |
124 |
125 |
126 | Note that most values in the karma.conf file can be overridden at the command line. For example, if you wanted to run Firefox instead of Chrome you could either change the value in karma.conf or keep the value the same and use this command:
127 | 128 |
129 | karma start --browsers Firefox
130 |
131 |
132 | I strongly suggest you take an existing karma.conf file like this one and adapt it to meet your needs. As long as you follow the conventions we outline in this guide, the only things you will likely want to change are:
133 | 134 |-
135 |
- files - to include all your custom code, dependencies and test code 136 |
- reporters - if you want test coverage you will need to include the 'coverage' reporter 137 |
- autoWatch and singleRun - most of the time you will want autoWatch=true and singleRun=false so that Karma will automatically re-run your tests as you make changes. However, if you are running Karma as part of a script like a git hook or continuous 138 | integration, then you will want to flip these two boolean values so that Karma only runs the tests once. 139 |
Basic Jasmine Spec Structure
142 |How is a basic spec organized?
143 | 144 |145 | File Structure 146 |
147 | 148 |In general, you want to have one test file for each and every non-test code file in your app. You should have a common naming scheme so that your build tools and test runners can pick out test files from non-test files. We are using one of the 149 | most common test file naming schemes: "{filename}.spec.js". So if you have a code file called "app.js", the file that contains all the tests for app.js would be called "app.spec.js". You will often see all test files in a separate directory 150 | from the rest of the code (usually called 'test'), but in the sample code we have put all specs right along side the code they are testing for your convenience.
151 | 152 |153 | Spec Code Structure 154 |
155 |In general, your spec files should follow this structure:
156 | 157 |
158 |
159 |
160 |
161 | Two things to note from this example.
162 | 163 |First, you should make liberal use of before, beforeEach, after and afterEach to set up and tear down the appropriate context for tests. Ideally you only have a couple lines of code within each 164 | it() function.
165 | 166 |The second thing to note is that the first parameter for the 167 | describe() and 168 | it() functions may be used by the test runner when tests are executed. For example, when this spec is run, some test runners may output:
169 | 170 |
171 | Unit: App App Abstract Route verifies state configuration
172 |
173 |
174 | So, make sure the string values are descriptive.
175 | 176 |Including a Module
177 |How do I inject a module in a spec?
178 | 179 |The first thing your spec should do is define all the Angular modules that are needed for the tests in that spec. This is done using the 180 | module() function that comes from the 181 | angular-mocks library. For example: 182 | 183 |
184 | beforeEach(module('myApp'));
185 |
186 |
187 | This code will enable the spec to test the code from the 188 | myApp module. It is best practice to use 189 | beforeEach() instead of just 190 | before() so that each test is essentially running from a blank slate. If you don't do this, the state from a previous test may bleed into another test and affect the results.
191 | 192 |Testing a Controller
193 |How do I instantiate a controller in a spec?
194 | 195 |Here is our controller that we want to test:
196 | 197 |
198 |
199 |
200 |
201 | The 202 | angular-mocks library provides a service called 203 | $controller that we can use to help us test our controllers. In the 204 | beforeEach() below, we are injecting 205 | $controller along with any other dependencies we need to instantiate our controller.
206 | 207 |
208 |
209 |
210 |
211 | Note that we are using the underscore syntax with 212 | _Messages_ to get a global reference to the 213 | Messages service. The underscores are ignored by the injector when the reference name is resolved.
214 | 215 |https://docs.angularjs.org/api/ngMock/function/angular.mock.inject
216 | 217 |Including a Service
218 |How do I inject a service in a spec?
219 | 220 |Testing an Angular service is a piece of cake. You can use the 221 | inject() function from angular-mocks to get a reference to either internal Angular core objects or any of your custom objects in the modules that are defined at the top of the spec. For example:
222 | 223 |
224 |
225 |
226 |
227 | As a best practice, we suggest injecting objects in 228 | beforeEach() and saving the object to a local variable. Then in the test we just reference that local variable.
229 | 230 |Testing a Template
231 |How do I test if an element is in the DOM?
232 | 233 |The most tricky thing to test with Angular is code within templates. That is why you should try to reduce the amount of code in your templates as much as possible. Even if you are really good about this, though, you will always have some template 234 | code that you want to test.
235 | 236 |You can split template testing into two categories. The first category includes templates that don't have any controllers, includes or custom directives. Essentially you are just testing logic that uses basic Angular expressions and core Angular 237 | directives. This use case is relatively easy to test. Take the following example:
238 | 239 | 240 |
241 |
242 |
243 |
244 |
245 |
246 | We want to have access to this HTML file in our tests, but it is generally a bad idea to make an http call within your tests if you can avoid it. Fortunately, there is a nice feature in Karma that allows us to automatically package all of 247 | our HTML template files into an Angular module that we can access easily in our tests. Just add the following to your 248 | karma.conf.js file:
249 | 250 |
251 |
252 |
253 |
254 | This will automatically package any file ending in .html within your src/ folder into an Angular module called myAppTemplates. Each template is accessible by using the 255 | $templateCache service. You can test this template simply by injecting the 256 | $compile service with some test data and then checking the resulting HTML:
257 | 258 |
259 |
260 |
261 |
262 | The second category of template testing is unfortunately more complex. We need to deal with dependencies within the template or surrounding the template such as a controller, the UI router, directives, etc. Let's look at one example of a more 263 | complex use case that includes a UI Router state and a controller:
264 | 265 |
266 |
267 |
268 |
269 | In order to test the template and associated controller code, we need to instantiate the controller and surrounding context. To that end, we have created a helper function called 270 | compileRouteTemplateWithController that does everything we need.
271 | 272 |
273 |
274 |
275 |
276 | We are using this helper function to get all the dependencies we need to run our tests. This includes creating scope, a controller and a render function. Feel free to use this and adapt it to your needs. Also, as an aside, whenever you see 277 | a lot of repetitive code within your tests, make sure you create your own helper functions.
278 | 279 |OK, now we have everything in place to test our template.
280 | 281 |
282 |
283 |
284 |
285 | The key part of this test was the use of the helper function to instantiate the template and all required dependencies:
286 | 287 |
288 |
289 |
290 |
291 |
292 | Testing a Route
293 |How do I test route changes?
294 | 295 |Testing a route essentially means testing that we configured the UI router $stateProvider correctly during the config phase. For example, given the following state configuration:
296 | 297 |
298 |
299 |
300 |
301 | Our basic strategy for testing is to use the 302 | $state.go() and 303 | $state.href() methods to modify the current state and then check to make sure the route is changed appropriately.
304 | 305 |
306 |
307 |
308 |
309 | Testing a Directive
310 |How do I set up a spec for a directive?
311 | 312 |Similar to how we test a template, we use the $compile service to help us test a directive. The key is to pass in a HTML snippet to 313 | $compile() that refers to the target directive. For example, if you had an element directive called 'experiment', you would simply call 314 | $compile("<experiment></experiment>"). You can see the full example here:
315 | 316 |
317 |
318 |
319 |
320 | And the spec.
321 |
322 |
323 |
324 |
325 |
326 | Note: depending on what your directive does, you may need to modify the HTML passed into 327 | $compile. For example, if the directive expects other attributes on the element or if you are testing a directive with transclusion (in which case you will want to put different snippets of HTML as children to the element that has 328 | the directive).
329 | 330 |Mocking
331 |How/Why do I mock a service call in a controller spec?
332 | 333 |We mentioned at the beginning that you can create both unit tests and integration tests with Karma. When you are writing a unit test, your goal is to test just one thing and either eliminate or mock out all other dependencies. In general, 334 | you can mock any object through the use of the 335 | $provide service. You use this service when you define the module you are using in your spec. For example: 336 | 337 |
338 |
339 |
340 |
341 | In this case, we are overriding the SimpleService object. Once we do this, any code that injects SimpleService will get our mock object instead of the actual SimpleService.
342 | 343 |There is one special case with mocking where Angular helps you out. Whenever you use 344 | $http to make a remote call, Angular has another service behind the scenes called 345 | $httpBackend that actually does all the hard work. The angular-mocks library has its own version of 346 | $httpBackend with a number of goodies to help us mock out calls to the back end. For example look at this code which makes an 347 | $http call:
348 | 349 |
350 |
351 |
352 |
353 | If we call 354 | getExperiments() in our test, it will make an actual http request to data/experiments.json. We can intercept that call with 355 | $httpBackend, however, and define what should be returned instead of making a remote call.
356 | 357 |
358 |
359 |
360 |
361 | Note that 362 | $httpBackend.flush() is needed because normally $http is asynchronous, but we want to execute our test in a synchronous fashion. The call to 363 | flush() will ensure that the 364 | .then() on the promise returned from $http will be executed immediately.
365 | 366 |Spying on Methods
367 |How do I determine if one method successfully calls another method?
368 |
How do I determine what arguments were included when I call a method?
Jasmine uses a spy to determine whether a method has been called and/or what arguments are set into a method call. So, for example:
371 | 372 |
373 |
374 |
375 |
376 |
377 | A spy only exists within the 378 | describe() or 379 | it() function where it has been defined.
380 | 381 |Advanced Spying
382 | 383 |In addition to simply seeing if a spy has been called, we can also define what value the spy should return (using returnValue()) or what fake function the spy should run instead of the target function (using callFake()). For example:
384 | 385 |
386 |
387 |
388 |
389 | And the spec.
390 |
391 |
392 |
393 |
394 |
395 | Testing a Promise
396 |How do I handle async operations in a spec?
397 | 398 |By default, each test runs synchronously. So, if you have any asynchronous operation, the test will complete before the operation completes. There are ways of handling specific use cases (for example 399 | $httpBackend.flush() as mentioned earlier), but you can also use the Jasmine 400 | done() function. For example:
401 | 402 |
403 |
404 |
405 |
406 | In this example, the test will not complete until 407 | done() is called. If 408 | done() contains a parameter, Jasmine treats that as an error and fails the test.
409 | 410 |One last note on async operations. You may have noticed in our examples a call to 411 | $rootScope.$digest(). This will force the digest cycle to run which is needed whenever we are testing anything athat involves watchers (so, anything with templates).
412 | 413 |Debugging
414 |How do I troubleshoot problems with my specs?
415 | 416 |Spec code is run in the browser just like any other client side code. So, how do you debug your Angular app? That's right, the Chrome/FireFox dev tools. For example, after running Karma with Chrome there should be a Chrome window open on 417 | your machine that contains the output of the test. To debug, simply open up the Chrome dev tools and refresh the page.
418 | 419 |Best Practices
420 |How do I know I am formatting my specs in the most efficient manner?
421 | 422 |Here is a quick list of best practices. Some of these we touched on earlier.
423 |-
424 |
- Use 425 | beforeEach() to set up the context for your tests. 426 |
- Make sure the string descriptions you put in 427 | describe() and 428 | it() make sense as output 429 |
- Use 430 | after() and 431 | afterEach() to cleanup your tests if there is any state that may bleed over. 432 |
- If any one test is over 10 lines of code, you may need to refactor the test 433 |
- If you find yourself repeating the same code for many tests, refactor the common code into a helper function 434 |
Resources
437 | 438 | 439 | 440 |If you want to do some more digging, here are 6 resources that will get you started.
441 |-
442 |
- 443 | Angular Unit Testing Docs 444 | 445 |
- 446 | Karma Docs 447 | 448 |
- 449 | Jasmine Docs 450 | 451 |
- 452 | Unit testing Angular applications 453 | 454 |
- 455 | Unit Testing in Angular: Services, Controllers & Providers 456 | 457 |
- 458 | Mocking Dependencies in Angular Tests 459 | 460 |