Unit Testing Angular Services, HTTP Calls and HTTP Interceptors

Posted by: Ravi Kiran , on 8/28/2020, in Category AngularJS
Views: 5901
Abstract: This Angular 10 tutorial will state the importance of unit testing Angular services. It also explains the process of unit testing services, HTTP calls and HTTP interceptors in an Angular application.

Angular services contain UI-independent reusable business logic of the application.

This logic could be used at multiple places in the application – say to receive or calculate data to be shown on the page. So, it is very important to make sure that the logic in the services is correct, or else this could result in issues at multiple places in the application.

Unit tests can be used to test the services by invoking the functionality directly.

As discussed in a previous article, unit testing can be used to invoke and test the behavior of a piece of code in isolation. The reusable logic written in services requires this kind of testing, as unit testing provides ways to test all possible scenarios by sending different types of data to the service methods.

Also, most applications use services to communicate with the backend APIs. It is important to make sure that the calls to these services are made correctly and their responses are correctly handled in the application. Unit tests help in checking the correctness in these calls.

Angular framework includes a testing module to test the API calls by providing mock responses. This setup can be used to effectively test whether the right set of APIs is called with correct parameters, and then test how the success and failures of the APIs are handled.

This Angular 10 tutorial will provide you with enough knowledge on setting up a test file to unit test a service as well as how to write unit test case for Angular 10 Services.

This tutorial will also show how the calls to backend APIs can be unit tested in Angular.

angular-unit-testing

Testing Services in Angular

The required setup to test any piece of Angular is already included with Angular CLI. My previous tutorial on testing Angular components goes through details of the setup and explains it. Let’s see how services can be tested by taking a simple example.

Consider the following service:

@Injectable({
  providedIn: "root"
})
export class CalculationsService {
  add(a: number, b: number): number {
    return a + b;
  }
}

This service is fairly easy to test, as it doesn’t have any dependencies, and the logic executed in the add method, is adding two numbers. We need to perform the following tasks to test this service:

  • Get an object of the service
  • Call the methods to test
  • Assert the results

The following code snippet gets an object of the CalculationsService:

describe('CalculationsService tests', () => {
  let calculationsSvc: CalculationsService;

  beforeEach(inject(
    [CalculationsService],
    (calcService: CalculationsService) => {
      calculationsSvc = calcService;
    }
  ));
});

The one thing to notice here is that we didn’t add the TestBed setup here. We didn’t do this as CalculationsService is provided in the root injector. Otherwise, if the service is provided in a module or in a component, we need to provide the service in the testing module configured with TestBed. The beforeEach block gets object of the service from the root injector. Now this object can be used to call the add method and test it.

The following snippet tests the add method:

it("should add two numbers", () => {
  let result = calculationsSvc.add(2, 3);
  expect(result).toEqual(5);
});

Angular Service Unit Testing Example with HttpClient

A service with dependencies requires some more amount of setup for testing.

As unit testing is the technique for testing a piece of code in isolation, the dependencies of the service have to be mocked so the dependency doesn’t become an obstacle while testing. One of the most common usages of the services is to interact with the backend APIs. It is needless to say that Angular applications use HttpClient to call the APIs, and Angular provides a mock implementation of this service to make it easier for the users of HttpClient to unit test their code.

Let’s write unit tests for the DataAccessService used in the article Getting Started with HTTP Client. Here is the complete code of the service:

import { Injectable } from "@angular/core";
import { Traveller } from "./traveller";
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { catchError } from "rxjs/operators";

import { Observable, throwError } from "rxjs";

const DATA_ACCESS_PREFIX: string = "api/travellers";

@Injectable({
  providedIn: 'root'
})
export class DataAccessService {
  constructor(private client: HttpClient) {}

  getTravellers(): Observable<Traveller[]> {
    return this.client.get<Traveller[]>(`${DATA_ACCESS_PREFIX}`).pipe(
      catchError((error: HttpErrorResponse) => {
        return throwError(
          `Error retrieving travellers data. ${error.statusText || "Unknown"} `
        );
      })
    );
  }

  deleteTraveller(id: number): Observable<any> {
    return this.client.delete<Traveller>(`${DATA_ACCESS_PREFIX}/${id}`).pipe(
      catchError((error: HttpErrorResponse) => {
        return throwError(
          `Error deleting travellers data. ${error.statusText || "Unknown"} `
        );
      })
    );
  }

  createTraveller(traveller: Traveller) {
    return this.client.post(`${DATA_ACCESS_PREFIX}`, traveller);
  }

  updateTraveller(traveller: Traveller, id: number) {
    return this.client.patch(`${DATA_ACCESS_PREFIX}/${id}`, traveller);
  }
}

As we can see, this service performs CRUD operations on a list of travellers that are made available through REST APIs. Let’s set the environment for testing this.

The following snippet does this:

import { TestBed, inject } from "@angular/core/testing";
import {
  HttpClientTestingModule,
  HttpTestingController
} from "@angular/common/http/testing";

import { DataAccessService } from "./data-access.service";
import { Traveller } from "./traveller";

describe("DataAccessService", () => {
  let httpTestingController: HttpTestingController;
  let dataAccessService: DataAccessService;
  let baseUrl = "api/travellers";
  let traveller: Traveller;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule]
    });

    httpTestingController = TestBed.get(HttpTestingController);
    traveller = {
      id: 2,
      firstName: "John",
      lastName: "Kelly",
      city: "Boston",
      country: "USA",
      age: 18
    };
  });

  beforeEach(inject(
    [DataAccessService],
    (service: DataAccessService) => {
      dataAccessService = service;
    }
  ));
});

The above snippet does the following tasks:

  • Imports the required objects. The HttpClientTestingModule and HttpTestingController are vital for testing the behavior of HttpClient.
    • The HttpClientTestingModule includes a mock implementation of HttpClient service, which doesn’t make the actual XHR calls, instead it provides a way to inspect the calls attempted
    • The HttpTestingController provides APIs to make sure that the HTTP calls are made, to provide mock response to the calls and to flush the requests, so that the subscribers of the observables would be invoked
  • Configures the testing module by importing the HttpClientTestingModule and gets the object of HttpTestingController
  • Creates a mock traveler object which will be used in the tests
  • Gets object of the DataAccessService

Now we have everything required to test the service. Let’s write a test to check the correctness of getTravellers method.

it("should return data", () => {
  let result: Traveller[];
  dataAccessService.getTravellers().subscribe(t => {
    result = t;
  });
  const req = httpTestingController.expectOne({
    method: "GET",
    url: baseUrl
  });

  req.flush([traveller]);

  expect(result[0]).toEqual(traveller);
});

The above test calls the getTravellers method and expects a GET call to be made to the baseUrl. Then it flushes the request with the data to be returned. This is when the call is completed and the subscribe method is called. At the end, it inspects if the request returned the correct data.

 

The getTravellers method should throw an error when the API fails. The HTTP failure case can be emulated using HttpTestingController. For this, we need to flush the request with an error message and a failure HTTP status instead of returning the data. The following snippet tests the failure case of getTravellers method:

it("should throw error", () => {
  let error: string;
  dataAccessService.getTravellers().subscribe(null, e => {
    error = e;
  });

  let req = httpTestingController.expectOne("api/travellers");
  req.flush("Something went wrong", {
    status: 404,
    statusText: "Network error"
  });

  expect(error.indexOf("Error retrieving travellers data") >= 0).toBeTruthy();
});

Notice the difference in handling the observable returned from the getTravellers method. We passed null for a success callback, as we know that this observable will never succeed. The error callback assigns the error to a variable so that it can be asserted.

The other methods of DataAccessService can be tested in the same way. Let’s test the createTraveller method to see how to test a POST call. This method should pass the object it receives to the REST API. The following snippet shows the unit test for this:

it("should call POST API to create a new traveller", () => {
  dataAccessService.createTraveller(traveller).subscribe();

  let req = httpTestingController.expectOne({ method: "POST", url: baseUrl });
  expect(req.request.body).toEqual(traveller);
});

The updateTraveller method invokes the PATCH API to update a traveller. We can test this method to check if the right parameter and body are sent to the API. The following snippet shows this test:

it("should call patch API to update a traveller", () => {
  dataAccessService.updateTraveller(traveller, traveller.id).subscribe();

  let req = httpTestingController.expectOne({
    method: "PATCH",
    url: `${baseUrl}/${traveller.id}`
  });
  expect(req.request.body).toEqual(traveller);
});

I leave the testing of deleteTraveller method as an assignment to the readers. You can also check the sample code to see how it is done.

Testing HTTP Interceptors

HTTP Interceptors are used to handle the tasks that have to be performed with every request going out of the application. Any mistake in the behavior of the interceptor may cause problems in every API request. Some of you may have started thinking how to do this, as interceptors are not directly invoked. They can be tested in the same way as they are used. We can make a request and see if the interceptor is invoked and performs the right action.

Let’s consider the following interceptor:

@Injectable()
export class LoggingInterceptorService implements HttpInterceptor {
  constructor(private logger: LoggerService) {}

  intercept(
    req: HttpRequest<any>,
    next: HttpHandler
  ): Observable<HttpEvent<any>> {
    req.headers.set('Authorization', 'auth-token');
    this.logger.info(`Calling API: ${req.url}`);
    return next.handle(req).pipe(
      tap(
        (data: HttpEvent<any>) => {
          this.logger.success(`Call to the API ${req.url} succeeded`);
        },
        (error: HttpErrorResponse) =>
        this.logger.error(`Call to the API ${req.url} failed with status ${error.status}`)
      )
    );
  }
}

The above interceptor logs messages when a request is made, a request succeeds or when a request fails. It uses the service LoggerService to log these messages. It also sets the Authorization token in the header of every request. For demo purpose, the above service assigns a hard-coded token; but in real life this token has to be read from localstorage or any place where the token is persisted before it is assigned to the header.

Setup for testing the LoggingInterceptor will involve the following:

  • Creating a mock for LoggingService
  • Configuring the testing module with:
    • HttpTestingModule imported to the test module
    • Providing the interceptor and the mock logging service
  • Get the references of HttpClient and HttpTestingController to make requests and to inspect them

The following snippet shows this setup:

import { TestBed } from "@angular/core/testing";
import {
  HttpClientTestingModule,
  HttpTestingController
} from "@angular/common/http/testing";
import { HTTP_INTERCEPTORS, HttpClient } from "@angular/common/http";
import { LoggingInterceptorService } from "./logging-interceptor.service";
import { LoggerService } from "./logger.service";

describe("LoggingInterceptorService tests", () => {
  let httpTestingController: HttpTestingController,
    mockLoggerSvc: any,
    httpClient: HttpClient;

  beforeEach(() => {
    mockLoggerSvc = {
      info: jasmine.createSpy("info"),
      success: jasmine.createSpy("success"),
      error: jasmine.createSpy("error")
    };

    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [
        {
          provide: HTTP_INTERCEPTORS,
          useClass: LoggingInterceptorService,
          multi: true
        },
        {
          provide: LoggerService,
          useValue: mockLoggerSvc
        }
      ]
    });

    httpClient = TestBed.get(HttpClient);
    httpTestingController = TestBed.get(HttpTestingController);
  });
});

Every test will make an HTTP request using httpClient and then flush the request using httpTestingController, so the request is completed and then the test will assert the behavior.

For every request made, the interceptor logs an info message and sets the Authorization header. Let’s write our first test to check if this is done correctly. The following snippet shows the test:

it("should log a message when an API is called and set the authorization header", () => {
  httpClient.get("api/travellers").subscribe();

  let req = httpTestingController.expectOne("api/travellers");
  req.flush([]);

  expect(mockLoggerSvc.info).toHaveBeenCalled();
  expect(mockLoggerSvc.info).toHaveBeenCalledWith(
    "Calling API: api/travellers"
  );
  expect(req.request.headers.get("Authorization")).toBeDefined();
});

As we see, the interceptor is not directly invoked here, rather its behavior is tested in the same way as it would run in an actual application.

Let’s write one more test to check if the interceptor logs the success message when an API succeeds. The following test shows this:

it("should log a success message when the API call is successful", () => {
  httpClient.get("api/travellers").subscribe();

  let req = httpTestingController.expectOne("api/travellers");
  req.flush([]);

  expect(mockLoggerSvc.success).toHaveBeenCalled();
  expect(mockLoggerSvc.success).toHaveBeenCalledWith(
    "Call to the API api/travellers succeeded"
  );
});

Testing the API failure case is much similar to the success case. The only difference will be that now we have to fail the request by passing an HTTP error code. The following snippet shows the failure case:

it("should log an error message when the API call fails ", () => {
  httpClient.get('api/travellers').subscribe();

  let req = httpTestingController.expectOne("api/travellers");
  req.error(null, { status: 404 });

  expect(mockLoggerSvc.error).toHaveBeenCalled();
  expect(mockLoggerSvc.error).toHaveBeenCalledWith(
    "Call to the API api/travellers failed with status 404"
  );
});

Conclusion

Unit testing is a vital part of software development. Because of its detailed and low-level nature, it helps in finding bugs early and fixing them.

Angular Services are created for reusability of data or business logic in an application, so it is important to make sure that the services work correctly. This tutorial explained how services, HTTP requests and the HTTP interceptors can be tested. I hope these techniques help in writing better tests in your Angular applications.

If you have any additional thoughts on Angular unit testing, please leave a comment if you are reading a web version of this article, or reach out to me on twitter.

This article was technically reviewed by Damir Arh.

Download the entire source code of this article from Github

This article has been editorially reviewed by Suprotim Agarwal.

Absolutely Awesome Book on C# and .NET

C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.

We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).

Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.

Click here to Explore the Table of Contents or Download Sample Chapters!

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Author
Rabi Kiran (a.k.a. Ravi Kiran) is a developer working on Microsoft Technologies at Hyderabad. These days, he is spending his time on JavaScript frameworks like AngularJS, latest updates to JavaScript in ES6 and ES7, Web Components, Node.js and also on several Microsoft technologies including ASP.NET 5, SignalR and C#. He is an active blogger, an author at SitePoint and at DotNetCurry. He is rewarded with Microsoft MVP (Visual Studio and Dev Tools) and DZone MVB awards for his contribution to the community


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!

Categories

JOIN OUR COMMUNITY

POPULAR ARTICLES

Tags

JQUERY COOKBOOK

jQuery CookBook