Get the "Migrating an Angular 1 App to Angular 2 book" for Free. Spread the word and download it now!

When I started developing and writing tests for Angularjs applications, everything felt natural. The tools were mature and I easily got used to developing applications in TDD (Test-Driven Development). It gave me a high level of confidence, knowing that my application was working as I had imagined. Just after Angular 2 came out and I learned the basics, I knew that the next step was to testing.

This article is the first part of a series in which I share my experiences testing different building blocks of an Angular application. We will start with simple use cases and then head for more complex ones.

You may wonder why it is so important to write tests.

Angular Testing Driven Development

With tests, we can ensure the correctness of our application: that the code does what it was designed to do. We can guard against someone breaking our code by refactoring or adding new features. This might have happened to you when someone added a small feature or equivalent code transformations and nothing worked afterwards. Writing tests can clarify the intention of the code by giving usage examples. It can also reveal design flaws. When a piece of code is hard to test, there might be a problem with the underlying architecture.

If you are new to Test-Driven Development, I would recommend reading the Test-Driven Development book by Kent Beck. It gives a nice overview about the concepts and best practices.

Choosing the framework to test Angular

Angular Testing Framework

The first thing we have to choose is the framework. The one suggested by Angular's core team is Jasmine. For a long time it was the only supported testing framework, because test setups were hard wired into the framework. Thanks to refactoring, now tests can also be written in Mocha, or any other framework that supports the beforeEach hook. This hook runs before every test run. If your framework of choice doesn't support it, you have to add the following code snippet to your setup:

import { resetFakeAsyncZone, TestBed } from '@angular/core/testing';

beforeEveryTestHook(() => {
  TestBed.resetTestingModule();
  resetFakeAsyncZone();
});

The first line within the hook resets the internal state of the Dependency Injection container. It clears out any given provider or module. If you are not familiar with Dependency Injection, I would recommend reading the official documentation about it.

The second one clears out any remaining zone that fakes asynchronous operations like setTimeout. Detailed articles can be found on the Thoughtram blog about zones: Understanding zones and Zones in Angular.

For this series, we will be using Jasmine as the test framework.

Writing the first Angular test

Let's look at our first service that will be tested.

import { Injectable } from '@angular/core';

@Injectable()
export class Engine {
  getHorsepower() {
    return 150;
  }

  getName() {
    return 'Basic engine';
  }
}

It has two getter methods and the Injectable decorator. The tests will check whether these getter methods work as intended. The decorator is needed to utilize dependency injection.

In Jasmine, we can group our tests with the describe method. Within this method, we can create test cases with the it function. It is advised to place one class per file (the service) and group the tests around it (with describe). We can further group test cases around methods of the class by placing describe statements inside the top describe block. For now we will only group our tests around the class.

import { Engine } from './engine.service';

describe('Engine', () => {
  it('should return it\'s horsepower', () => {
    let subject = new Engine();

    expect(subject.getHorsepower()).toEqual(150);
  });
});

In this setup, only plain instantiation is used; we will introduce dependency injection later. For basic services, plain instantiation can be enough.

We call the getHorsepower method of the engine and check that it's equal to the expected value.

The first test is green and has been passed. Let's write another one for the getName method.

it('should return it\'s horsepower', () => {
  let subject = new Engine();

  expect(subject.getName()).toEqual('Basic engine');
});

If you run the tests, a similar output will be on the terminal.

Angular First Tests

Both tests have been passed; it is time to refactor. There is duplication at the start of each Angular test. Instantiation is exactly the same, we can move it out into a setup block.

describe('Engine', () => {
  let subject: Engine;

  beforeEach(() => {
    subject = new Engine();
  });

  it('should return it\'s horsepower', () => {
    expect(subject.getHorsepower()).toEqual(150);
  });

  it('should return it\'s horsepower', () => {
    expect(subject.getName()).toEqual('Basic engine');
  });
});

The subject variable is declared at the start of the describe block, and the creation of the service is moved to the beforeEach block. This way we don't have to do it manually every time. It is common to move the creation of the test case subject to a separate method, because it offloads the tests and makes them more readable.

Using Dependency Injection

Creating services directly can be good if the subject under test has no or few, dependencies. But if it has multiple dependencies, or a deeper dependency tree, setting up all the classes becomes tedious. For these situations, we can use Angular's dependency injection management.

The Car class uses the Engine class in the constructor and its instance in the getName method.

import { Injectable } from '@angular/core';
import { Engine } from './engine.service';

@Injectable()
export class Car {
  constructor(private engine: Engine) {}

  getName() {
    return `Car with ${this.engine.getName()}(${this.engine.getHorsepower()} HP)`;
  }
}

We check for the getName method's output in the test after we set up the dependency injection container.

import { TestBed, inject } from '@angular/core/testing';
import { Engine } from './engine.service';
import { Car } from './car.service';

describe('Car', () => {
  let subject: Car;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [Engine, Car]
    });
  });

  beforeEach(inject([Car], (car: Car) => {
    subject = car;
  }));

  it('should display name with engine', () => {
    expect(subject.getName()).toEqual('Car with Basic engine(150 HP)');
  });
});

The difference here is that we configure the TestBed with the provided services in the configureTestingModule method. Only these classes can be instantiated with the inject method. If we try to request something else, we get an error saying it is an unknown provider.

Angular Testing Error

We can request instances of the services in an array from the inject method. In the callback, we get the instances in the same order as in the dependency array with the first parameter. The type hint inside the callback is only for IDE completion; it also works without it. In the example, it is placed inside the beforeEach function, but it can also be added to the it block.

it('should display name with engine', inject([Car], (car: Car) => {
  expect(car.getName()).toEqual('Car with Basic engine(150 HP)');
}));

Mocking

In unit tests for Angular, we want to execute the code in isolation. This means it is not dependent on big, complex objects and is not calling methods that rely on external systems (like HTTP calls or database access). In these cases, we want to simulate the original behavior while skipping the underlying implementation.

When achieved, it is called mocking. We don't have to do it manually. Jasmine provides tools to make it work. Let's assume that the method of the Engine class has a call through to the server and we want to mock it.

...
beforeEach(() => {
  TestBed.configureTestingModule({
    providers: [Engine, Car]
  });

  spyOn(Engine.prototype, 'getHorsepower').and.returnValue(400);
  spyOn(Engine.prototype, 'getName').and.returnValue('V8 engine');
});

...

it('should display name with engine', () => {
  expect(subject.getName()).toEqual('Car with V8 engine(400 HP)');
});

We mock the class methods by calling the spyOn method on the class's prototype. We also alter the return value of the function. This way the original method never gets called. When mocking the prototype, it affects every instance of the class.

Using Dependency Injection for Mocking

The previous solution for mocking can be achieved also with Angular's dependency injection mechanism. We pass the Engine class as the provider token, but create the instances with a fake class.

@Injectable()
class V8Engine {
  getHorsepower() {
    return 400;
  }

  getName() {
    return 'V8 engine';
  }
}

Then we pass this fake class to the setup.

beforeEach(() => {
  TestBed.configureTestingModule({
    providers: [
      { provide: Engine, useClass: V8Engine },
      Car
    ]
  });
});

With this setup, the inject call will return an instance of V8Engine when asked for Engine. We can also use useFactory with a callback, or useValue with an instance, to accomplish the same result. The only drawback here is that every method of the class must be implemented and changed whenever the original class changes. The original class can be extended optionally in order to override only specific methods.

Aside: Authenticate an Angular App and Node API with Auth0

We can protect our applications and APIs so that only authenticated users can access them. Let's explore how to do this with an Angular application and a Node API using Auth0. You can clone this sample app and API from the angular-auth0-aside repo on GitHub.

Auth0 login screen

Features

The sample Angular application and API has the following features:

  • Angular application generated with Angular CLI and served at http://localhost:4200
  • Authentication with auth0.js using a login page
  • Node server protected API route http://localhost:3001/api/dragons returns JSON data for authenticated GET requests
  • Angular app fetches data from API once user is authenticated with Auth0
  • Profile page requires authentication for access using route guards
  • Authentication service uses subjects to provide authentication and profile data to the app

Sign Up for Auth0

You'll need an Auth0 account to manage authentication. You can sign up for a free account here. Next, set up an Auth0 application and API so Auth0 can interface with an Angular app and Node API.

Set Up an Auth0 Application

  1. Go to your Auth0 Dashboard: Applications section and click the "+ Create Application" button.
  2. Name your new app and select "Single Page Web Applications".
  3. In the Settings for your new Auth0 app, add http://localhost:4200/callback to the Allowed Callback URLs.
  4. Add http://localhost:4200 to both the Allowed Web Origins and Allowed Logout URLs. Click the "Save Changes" button.
  5. If you'd like, you can set up some social connections. You can then enable them for your app in the Application options under the Connections tab. The example shown in the screenshot above uses username/password database, Facebook, Google, and Twitter.

Note: Set up your own social keys and do not leave social connections set to use Auth0 dev keys or you will encounter issues with token renewal.

Set Up an API

  1. Go to APIs in your Auth0 dashboard and click on the "Create API" button. Enter a name for the API. Set the Identifier to your API endpoint URL. In this example, this is http://localhost:3001/api/. The Signing Algorithm should be RS256.
  2. You can consult the Node.js example under the Quick Start tab in your new API's settings. We'll implement our Node API in this fashion, using Express, express-jwt, and jwks-rsa.

We're now ready to implement Auth0 authentication on both our Angular client and Node backend API.

Dependencies and Setup

The Angular app utilizes the Angular CLI. Make sure you have the CLI installed globally:

$ npm install -g @angular/cli

Once you've cloned the project on GitHub, install the Node dependencies for both the Angular app and the Node server by running the following commands in the root of your project folder:

$ npm install
$ cd server
$ npm install

The Node API is located in the /server folder at the root of our sample application.

Find the config.js.example file and remove the .example extension from the filename. Then open the file:

// server/config.js (formerly config.js.example)
module.exports = {
  CLIENT_DOMAIN: '[YOUR_AUTH0_DOMAIN]', // e.g., 'you.auth0.com'
  AUTH0_AUDIENCE: 'http://localhost:3001/api/'
};

Change the CLIENT_DOMAIN value to your full Auth0 domain and set the AUTH0_AUDIENCE to your audience (in this example, this is http://localhost:3001/api/). The /api/dragons route will be protected with express-jwt and jwks-rsa.

Note: To learn more about RS256 and JSON Web Key Set, read Navigating RS256 and JWKS.

Our API is now protected, so let's make sure that our Angular application can also interface with Auth0. To do this, we'll activate the src/environments/environment.ts.example file by deleting .example from the file extension. Then open the file and change the [YOUR_CLIENT_ID] and [YOUR_AUTH0_DOMAIN] strings to your Auth0 information:

// src/environments/environment.ts (formerly environment.ts.example)
...
export const environment = {
  production: false,
  auth: {
    CLIENT_ID: '[YOUR_CLIENT_ID]',
    CLIENT_DOMAIN: '[YOUR_AUTH0_DOMAIN]', // e.g., 'you.auth0.com'
    ...
  }
};

Our app and API are now set up. They can be served by running ng serve from the root folder and node server from the /server folder. The npm start command will run both at the same time for you by using concurrently.

With the Node API and Angular app running, let's take a look at how authentication is implemented.

Authentication Service

Authentication logic on the front end is handled with an AuthService authentication service: src/app/auth/auth.service.ts file. We'll step through this code below.

// src/app/auth/auth.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, bindNodeCallback, of } from 'rxjs';
import * as auth0 from 'auth0-js';
import { environment } from './../../environments/environment';
import { Router } from '@angular/router';

@Injectable()
export class AuthService {
  // Create Auth0 web auth instance
  // @TODO: Update environment variables and remove .example
  // extension in src/environments/environment.ts.example
  private _Auth0 = new auth0.WebAuth({
    clientID: environment.auth.CLIENT_ID,
    domain: environment.auth.CLIENT_DOMAIN,
    responseType: 'id_token token',
    redirectUri: environment.auth.REDIRECT,
    audience: environment.auth.AUDIENCE,
    scope: 'openid profile email'
  });
  // Track whether or not to renew token
  private _authFlag = 'isLoggedIn';
  // Create stream for token
  token$: Observable<string>;
  // Create stream for user profile data
  userProfile$ = new BehaviorSubject<any>(null);
  // Authentication navigation
  onAuthSuccessUrl = '/';
  onAuthFailureUrl = '/';
  logoutUrl = environment.auth.LOGOUT_URL;
  // Create observable of Auth0 parseHash method to gather auth results
  parseHash$ = bindNodeCallback(this._Auth0.parseHash.bind(this._Auth0));
  // Create observable of Auth0 checkSession method to
  // verify authorization server session and renew tokens
  checkSession$ = bindNodeCallback(this._Auth0.checkSession.bind(this._Auth0));

  constructor(private router: Router) { }

  login() {
    this._Auth0.authorize();
  }

  handleLoginCallback() {
    if (window.location.hash && !this.authenticated) {
      this.parseHash$().subscribe(
        authResult => {
          this._setAuth(authResult);
          window.location.hash = '';
          this.router.navigate([this.onAuthSuccessUrl]);
        },
        err => this._handleError(err)
      )
    }
  }

  private _setAuth(authResult) {
    // Observable of token
    this.token$ = of(authResult.accessToken);
    // Emit value for user data subject
    this.userProfile$.next(authResult.idTokenPayload);
    // Set flag in local storage stating this app is logged in
    localStorage.setItem(this._authFlag, JSON.stringify(true));
  }

  get authenticated(): boolean {
    return JSON.parse(localStorage.getItem(this._authFlag));
  }

  renewAuth() {
    if (this.authenticated) {
      this.checkSession$({}).subscribe(
        authResult => this._setAuth(authResult),
        err => {
          localStorage.removeItem(this._authFlag);
          this.router.navigate([this.onAuthFailureUrl]);
        }
      );
    }
  }

  logout() {
    // Set authentication status flag in local storage to false
    localStorage.setItem(this._authFlag, JSON.stringify(false));
    // This does a refresh and redirects back to homepage
    // Make sure you have the logout URL in your Auth0
    // Dashboard Application settings in Allowed Logout URLs
    this._Auth0.logout({
      returnTo: this.logoutUrl,
      clientID: environment.auth.CLIENT_ID
    });
  }

  private _handleError(err) {
    if (err.error_description) {
      console.error(`Error: ${err.error_description}`);
    } else {
      console.error(`Error: ${JSON.stringify(err)}`);
    }
  }

}

This service uses the auth config variables from environment.ts to instantiate an auth0.js WebAuth instance. Next an _authFlag member is created, which is simply a flag that we can store in local storage. It tells us whether or not to attempt to renew tokens with the Auth0 authorization server (for example, after a full-page refresh or when returning to the app later). All it does is state, "This front-end application thinks this user is authenticated" and then allows us to apply logic based on that estimation and verify whether or not it's accurate.

We'll add and type a token$ observable, which will provide a stream of the access token string. This is for use with the token interceptor. We don't want our interceptor to utilize a stream that emits a default value without any useable values. We'll declare token$ in our _setAuth() method below, when the access token becomes available.

We will use an RxJS BehaviorSubject to provide a stream of the user profile that you can subscribe to anywhere in the app. We'll also store some paths for navigation so the app can easily determine where to send users when authentication succeeds, fails, or the user has logged out.

The next thing that we'll do is create observables of the auth0.js methods parseHash() (which allows us to extract authentication data from the hash upon login) and checkSession() (which allows us to acquire new tokens when a user has an existing session with the authorization server). Using observables with these methods allows us to easily publish authentication events and subscribe to them within our Angular application.

We'll create observables of the callbacks from these two auth0.js methods using using RxJS's bindNodeCallback. In order to preserve the scope of this, we'll bind() it like so:

bindNodeCallback(this._Auth0.parseHash.bind(this._Auth0))

The login() method authorizes the authentication request with Auth0 using the environment config variables. A login page will be shown to the user and they can then authenticate.

Note: If it's the user's first visit to our app and our callback is on localhost, they'll also be presented with a consent screen where they can grant access to our API. A first party client on a non-localhost domain would be highly trusted, so the consent dialog would not be presented in this case. You can modify this by editing your Auth0 Dashboard API Settings. Look for the "Allow Skipping User Consent" toggle.

We'll receive accessToken, expiresIn, and idTokenPayload in the URL hash from Auth0 when returning to our app after authenticating at the login page. The handleLoginCallback() method subscribes to the parseHash$() observable to stream authentication data (_setAuth()) by creating our token$ observable and emitting a value for the userProfile$ behavior subject. This way, any subscribed components in the app are informed that the token and user data has been updated. The _authFlag is also set to true and stored in local storage so if the user returns to the app later, we can check whether to ask the authorization server for a fresh token. Essentially, the flag serves to tell the authorization server, "This app thinks this user is authenticated. If they are, give me their data." We check the status of the flag in local storage with the accessor method authenticated.

Note: The user profile data takes the shape defined by OpenID standard claims.

The renewAuth() method, if the _authFlag is true, subscribes to the checkSession$() observable to ask the authorization server if the user is indeed authorized (we can pass arguments to this observable as we would to the auth0.js function). If they are, fresh authentication data is returned and we'll run the _setAuth() method to update the necessary authentication streams in our app. If the user is not authorized with Auth0, the _authFlag is removed and the user will be redirected to the URL we set as the authentication failure location.

Next, we have a logout() method that sets the _authFlag to false and logs out of the authentication session on Auth0's server. The Auth0 logout() method then redirects back to the location we set as our logoutUrl.

Once AuthService is provided in app.module.ts, its methods and properties can be used anywhere in our app, such as the home component.

Callback Component

The callback component is where the app is redirected after authentication. This component simply shows a loading message until the login process is completed. It executes the authentication service's handleLoginCallback() method to parse the hash and extract authentication information.

// src/app/callback/callback.component.ts
import { Component, OnInit } from '@angular/core';
import { AuthService } from '../auth/auth.service';

@Component({
  selector: 'app-callback',
  template: `<div>Loading...</div>`,
  styles: []
})
export class CallbackComponent implements OnInit {
  constructor(private auth: AuthService) { }

  ngOnInit() {
    this.auth.handleLoginCallback();
  }

}

Making Authenticated API Requests

In order to make authenticated HTTP requests, it's necessary to add an Authorization header with the access token to our outgoing requests. Note that the api.service.ts file does not do this.

Instead, this functionality is in an HTTP interceptor service called token.interceptor.ts.

// src/app/auth/token.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { AuthService } from './auth.service';
import { Observable } from 'rxjs';
import { mergeMap } from 'rxjs/operators';

@Injectable()
export class InterceptorService implements HttpInterceptor {
  constructor(private auth: AuthService) { }

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    // @NOTE: If you have some endpoints that are public
    // and do not need Authorization header, implement logic
    // here to accommodate that and conditionally let public
    // requests pass through based on your requirements
    return this.auth.token$
      .pipe(
        mergeMap(token => {
          if (token) {
            const tokenReq = req.clone({
              setHeaders: { Authorization: `Bearer ${token}` }
            });
            return next.handle(tokenReq);
          }
        })
      );
  }
}

As mentioned above, we can return the token$ observable to acquire a token, then clone the outgoing HTTP request and attach an Authorization header before sending the request on its way.

The interceptor should be provided like so in the app-routing.module.ts file:

// src/app/app-routing.module.ts
...
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { InterceptorService } from './auth/token.interceptor';
...

@NgModule({
  imports: [...],
  providers: [
    ...,
    {
      provide: HTTP_INTERCEPTORS,
      useClass: InterceptorService,
      multi: true
    }
  ],
  ...
})
export class AppRoutingModule {}

Note: We set multi to true because we could implement multiple interceptors, which would run in the order of declaration.

Final Touches: Route Guard and Profile Page

A profile page component can show an authenticated user's profile information. However, we only want this component to be accessible if the user is logged in.

With an authenticated API request and login/logout implemented in the Home component, the final touch is to protect our profile route from unauthorized access. The auth.guard.ts route guard can check authentication and activate routes conditionally. The guard is implemented on specific routes of our choosing in the app-routing.module.ts file like so:

// src/app/app-routing.module.ts
...
import { AuthGuard } from './auth/auth.guard';
...

@NgModule({
  imports: [
    RouterModule.forRoot([
      ...,
      {
        path: 'profile',
        component: ProfileComponent,
        canActivate: [
          AuthGuard
        ]
      },
      ...
    ])
  ],
  providers: [
    AuthGuard,
    ...
  ],
  ...
})
export class AppRoutingModule {}

To Do: Elegant Error Handling

Now that the primary functionality is there, you'll want to think about gracefully handling and reacting to errors. Some functionality will need to be implemented for this. The errors are there to react to, but you'll want to consider how you prefer to respond to them when they occur.

More Resources

That's it! We have an authenticated Node API and Angular application with login, logout, profile information, and a protected route. To learn more, check out the following resources:

Conclusion: What we've learned about Angular Testing

In this tutorial, we managed to:

  • create a basic service and write tests for it
  • use dependency injection in tests
  • fake dependencies with Jasmine
  • fake dependencies with dependency injection

If you follow the steps introduced in this article and write tests for your Angular application, you can sleep safe and sound. The code will work as intended and when someone accidentally breaks it, the tests will warn him that those changes are unsafe and shouldn't be committed until the tests are green again.

Jasmine will help you along the way with its easy syntax and batteries included (assertion and mocking library).

I hope this has convinced you that writing tests in Angular is not an overly complicated thing.

To make the start even easier, the code is available in this GitHub repository.