Testing Quickstart

Angular Unit Testing Quick Start

Introduction

Why are you doing this?

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, 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 do I instantiate my controller? What if it has a service dependency? Aaaaaaaaargh!

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.

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

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 repo. Nothing would make me happier! Enjoy!

The Sample Project

Where can I see the actual tests?

The companion repository for this quick start guide can be found here https://github.com/simpulton/angular-testing-quick-start

Unit Testing vs E2E

How does Karma differ from Protractor?

I like to split my tests up into three different categories:

  1. 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 'my-button') and expected results (ex. after 200ms a new window should appear that says "Thank You").
  2. 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 a set of parameters and then checks to make sure the results match expected values.
  3. Unit Tests - These are the same as integration tests except you take extra steps toensure 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 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.

Protractor is the tool you will use for end-to-end tests while Karma handles integration and unit testing.

Installing Karma

How do I install Karma?

            npm install -g karma
          

Configuring Karma

How do does Karma know which files to include?

            karma init karma.conf.js
          

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:

  • Any angular libraries used by your code
  • All your custom code
  • All your test specs

Below is an example of a full karma.conf file.

            

            
          

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:

            karma start --browsers Firefox
          

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:

  • files - to include all your custom code, dependencies and test code
  • reporters - if you want test coverage you will need to include the 'coverage' reporter
  • 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 integration, then you will want to flip these two boolean values so that Karma only runs the tests once.

Basic Jasmine Spec Structure

How is a basic spec organized?

File Structure

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 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 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.

Spec Code Structure

In general, your spec files should follow this structure:

            
          

Two things to note from this example.

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 it() function.

The second thing to note is that the first parameter for the describe() and 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:

            Unit: App App Abstract Route verifies state configuration
          

So, make sure the string values are descriptive.

Including a Module

How do I inject a module in a spec?

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 module() function that comes from the angular-mocks library. For example:

              beforeEach(module('myApp'));
            

This code will enable the spec to test the code from the myApp module. It is best practice to use beforeEach() instead of just 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.

Testing a Controller

How do I instantiate a controller in a spec?

Here is our controller that we want to test:

              
            

The angular-mocks library provides a service called $controller that we can use to help us test our controllers. In the beforeEach() below, we are injecting $controller along with any other dependencies we need to instantiate our controller.

              
            

Note that we are using the underscore syntax with _Messages_ to get a global reference to the Messages service. The underscores are ignored by the injector when the reference name is resolved.

https://docs.angularjs.org/api/ngMock/function/angular.mock.inject

Including a Service

How do I inject a service in a spec?

Testing an Angular service is a piece of cake. You can use the 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:

              
            

As a best practice, we suggest injecting objects in beforeEach() and saving the object to a local variable. Then in the test we just reference that local variable.

Testing a Template

How do I test if an element is in the DOM?

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 code that you want to test.

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 directives. This use case is relatively easy to test. Take the following example:

              
            

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 our HTML template files into an Angular module that we can access easily in our tests. Just add the following to your karma.conf.js file:

              
            

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 $templateCache service. You can can test this template simply by injecting the $compile service with some test data and then checking the resulting HTML:

              
            

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 complex use case that includes a UI Router state and a controller:

              
            

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 compileRouteTemplateWithController that does everything we need.

              
            

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 a lot of repetitive code within your tests, make sure you create your own helper functions.

OK, now we have everything in place to test our template.

              
            

The key part of this test was the use of the helper function to instantiate the template and all required dependencies:

              
            

Testing a Route

How do I test route changes?

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:

              
            

Our basic strategy for testing is to use the $state.go() and $state.href() methods to modify the current state and then check to make sure the route is changed appropriately.

              
            

Testing a Directive

How do I set up a spec for a directive?

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 $compile() that refers to the target directive. For example, if you had an element directive called 'experiment', you would simply call $compile("<experiment></experiment>"). You can see the full example here:

              
            
And the spec.
              
            

Note: depending on what your directive does, you may need to modify the HTML passed into $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 the directive).

Mocking

How/Why do I mock a service call in a controller spec?

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, you can mock any object through the use of the $provide service. You use this service when you define the module you are using in your spec. For example:

                
              

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.

There is one special case with mocking where Angular helps you out. Whenever you use $http to make a remote call, Angular has another service behind the scenes called $httpBackend that actually does all the hard work. The angular-mocks library has its own version of $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 $http call:

                
              

If we call getExperiments() in our test, it will make an actual http request to data/experiments.json. We can intercept that call with $httpBackend, however, and define what should be returned instead of making a remote call.

                
              

Note that $httpBackend.flush() is needed because normally $http is asynchronous, but we want to execute our test in a synchronous fashion. The call to flush() will ensure that the .then() on the promise returned from $http will be executed immediately.

Spying on Methods

How do I determine if one method successfully calls another method?
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:

                
              

A spy only exists within the describe() or it() function where it has been defined.

Advanced Spying

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:

                
              
And the spec.
                
              

Testing a Promise

How do I handle async operations in a spec?

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 $httpBackend.flush() as mentioned earlier), but you can also use the Jasmine done() function. For example:

                
              

In this example, the test will not complete until done() is called. If done() contains a parameter, Jasmine treats that as an error and fails the test.

One last note on async operations. You may have noticed in our examples a call to $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).

Debugging

How do I troubleshoot problems with my specs?

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 your machine that contains the output of the test. To debug, simply open up the Chrome dev tools and refresh the page.

Best Practices

How do I know I am formatting my specs in the most efficient manner?

Here is a quick list of best practices. Some of these we touched on earlier.

  • Use beforeEach() to set up the context for your tests.
  • Make sure the string descriptions you put in describe() and it() make sense as output
  • Use after() and afterEach() to cleanup your tests if there is any state that may bleed over.
  • If any one test is over 10 lines of code, you may need to refactor the test
  • If you find yourself repeating the same code for many tests, refactor the common code into a helper function

Resources

If you want to do some more digging, here are 6 resources that will get you started.