Testing Angular Services - A Complete Introduction For Beginners


How do you test Angular services?

And suck up those bugs like a vacuum cleaner on steroids. 🤓

Want to go straight to the code? Then click here

Testing Angular services.

test

How do you do it?

How do you check to make sure that your Angular services are working as intended?

And that they're bug free?

How do you test them to discover any hidden bugs? And fix them before your Angular application is deployed?

In this complete introduction to testing Angular services I'll show you how to do just that. We'll start by testing a very simple service and then advance into testing services that depend on other services, like Angular's HttpClient for example. As well as testing services that return Observables and Promises.

And the cool thing is that Angular services are one of the easiest pieces of an Angular application to test. Plus testing Angular services is one of the best places to aim your guns.

Table of content

Why do we test services?

Because they're easy to test?

Or maybe a better question would be, should we even write any kind of tests for our Angular application?

Testing your code is not easy to learn or practice. Especially for the young kids on the block that have never written more than a few “Hello World” programs and for loops.

That’s why new developers, who weren’t taught the importance of testing their code, develop the bad habit of never testing it. Except manually of course. And the problem is, that many professional colleges never teach budding programmers to test their code. And the few exceptions make the mistake of never teaching them how to properly test their code.

Add to the problem that the demand for “professional” software engineers is currently doubling almost every 5 years. Which means that half of the active army of programmers that are hammering their keyboards right now, as you read this book, have 5 years of experience or less.

Our industry is in a constant upheaval.

It's groaning with growing pains.

The fact that you're reading an article about testing your code (specifically Angular services) is a sign that you care about your profession. You obviously want to become better at what you do. You want to build applications that you are proud of. And part of that process is making sure the code you write is properly tested.

But what does this have to do with testing Angular services?

In most Angular applications the Angular services are the back-bone of the application (depending on how you look at it).

Services are often used to fetch data, manage the application state, handle authentication and authorization and a host of other things. The services should be where your business logic lives and that's why we test Angular services.

DOM tests tend to be brittle, and they're useful for some cases, but the business logic is where you want to really aim your guns and if an Angular application is properly designed you'll find the business logic inside of the services.

First basic example. Writing first test

Let's say we have a basic storage service that is used to store key/value pairs for our application.

It looks like this.

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

@Injectable({
  providedIn: 'root'
})
export class StorageService {

  constructor() { }

  setValue(key: string, value: string) {
    localStorage.setItem(key, value);
  }

  getValue(key:string): string {
    return localStorage.getItem(key);
  }

  clearStorage() {
    localStorage.clear();
  }
}

How do we test this?

Since I used the Angular CLI to generate the service I can open the storage.service.spec.ts file and discover that it already created a basic test for me.

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

import { StorageService } from './storage.service';

describe('StorageService', () => {
  let service: StorageService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(StorageService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });
});

One of the bigger challenges when unit testing Angular applications is preparing the object for testing. Once it's prepared then the actual testing is usually very easy.

Fortunately for us Angular services are easy to prepare for testing. In the test above you'll notice that we've created a testing module with the help of TestBed. Then we injected our StorageService into that testing module. And now it's ready for testing.

Here's our tests.

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

import { StorageService } from './storage.service';

describe('StorageService', () => {
  let service: StorageService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(StorageService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('should store a value', () => {
    let key = "name";
    let value = "Daniel";

    service.setValue(key, value);

    let result = localStorage.getItem(key);

    expect(result).toEqual(value);
  });

  it('should get value', () => {
    let key = "email";
    let value = "email@email.com";

    localStorage.setItem(key, value);

    let result = service.getValue(key);

    expect(result).toEqual(value);
  });

  it('should clear everything', () => {
    service.clearStorage();
    var result = localStorage.length;
    expect(result).toEqual(0);
  });
});

Now if we run...

ng test

....we'll see a Chrome browser launch, run our tests, and display a green success message.

How to test Angular services with dependencies

But what if we decide to use a remote storage server instead of local storage?

The first step is to inject the HTTP client like this.

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class StorageService {

  constructor(private httpClient: HttpClient) { }

  setValue(key: string, value: string) {
    localStorage.setItem(key, value);
  }

  getValue(key:string): string {
    return localStorage.getItem(key);
  }

  clearStorage() {
    localStorage.clear();
  }
}

But we immediately get an injection error. 😱

null%20injection%20error

What now?

This is where mocking comes handy. We can use things like mocks and spies to create fake versions of the dependencies and properly test our Angular service.

So... back to the test file. Here's how we fix the null injection error. I've excluded the actual tests to keep the example simple.

import { HttpClient } from '@angular/common/http';
import { TestBed } from '@angular/core/testing';

import { StorageService } from './storage.service';

describe('StorageService', () => {
  let service: StorageService;
  let httpSpy: jasmine.SpyObj<HttpClient>;

  beforeEach(() => {
    httpSpy = jasmine.createSpyObj('HttpClient', ['get', 'post']);
    TestBed.configureTestingModule({
      providers: [
        {
          provide: HttpClient, useValue: httpSpy
        }
      ]
    });
    service = TestBed.inject(StorageService);    
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });
});

So what have we just done?

First, we used jasmine to create a spy object that pretends to be the HTTP client. Then we injected our spy into the testing module.

And wha-da-ya-know? Our injection errors vanished!

Angular how to test service observable.

Now it's time to refactor our storage service to call the remote API instead of using local storage.

Here's what it looks like.

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class StorageService {

  constructor(private httpClient: HttpClient) { }

  setValue(key: string, value: string): Observable<any> {
    return this.httpClient.post("https://storageservice.com", {key: value});
  }

  getValue(key:string): Observable<string> {
    return this.httpClient.get<string>(`https://storageservice.com?key=${key}`);
  }

  clearStorage(): Observable<any> {
    return this.httpClient.delete(`https://storageservice.com`);
  }
}

But now all of our tests are broken. 😩

How do we fix them?

Here's our refactored test file.

import { HttpClient } from '@angular/common/http';
import { fakeAsync, TestBed } from '@angular/core/testing';
import { of } from 'rxjs';

import { StorageService } from './storage.service';

describe('StorageService', () => {
  let service: StorageService;
  let httpSpy: jasmine.SpyObj<HttpClient>;

  beforeEach(() => {
    httpSpy = jasmine.createSpyObj('HttpClient', ['get', 'post', 'delete']);
    TestBed.configureTestingModule({
      providers: [
        {
          provide: HttpClient, useValue: httpSpy
        }
      ]
    });
    service = TestBed.inject(StorageService);    
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('should store a value', fakeAsync(() => {
    let key = "name";
    let value = "Daniel";

    httpSpy.post.and.returnValue(of(true));
    httpSpy.get.and.returnValue(of(value));

    service.setValue(key, value);

    let result = localStorage.getItem(key);

    expect(result).toEqual(value);
    expect(httpSpy.post).toHaveBeenCalled();
  }));

  it('should get value', fakeAsync((done: DoneFn) => {
    let key = "email";
    let value = "email@email.com";

    httpSpy.post.and.returnValue(of(true));
    httpSpy.get.and.returnValue(of(value));

    service.setValue(key, value);

    service.getValue(key).subscribe((result) => {
      expect(result).toEqual(value);
      expect(httpSpy.get).toHaveBeenCalled();
      done;
    });    
  }));

  it('should clear everything', fakeAsync((done: DoneFn) => {
    httpSpy.delete.and.returnValue(of(true));

    service.clearStorage().subscribe((result) => {      
      expect(httpSpy.delete).toHaveBeenCalled();
      done;
    });
  }));
});

Testing asynchronous code can make your head spin at first but it's actually pretty simple once you get the hang of things.

So, what have we just done?

There are a couple important pieces in the example above that may not go unnoticed.

The first one is the fakeAsync function. That function is used to wrap any test with asynchronous operations. It's a tremendous tool when testing Observable's and Promises.

The next is the fact that we set up mock return values for our httpSpy. Notice how I told the httpSpy to return specific values based on how it was called? This is the power of mocking dependencies.

And last of all, notice how we use an expect statement to expect that the httpSpy was called and being used.

Conclusion

And that, my friend, is the complete introduction for beginners on how to start testing Angular services.

What do you think of testing Angular services? Is it worth it?

Questions or comments?

Go ahead and drop a message below in the comments section.

signature

Angular Consultant