Intercepting Http Requests-- Using And Testing Angular's HttpClient
Alisa

Alisa @alisaduncan

About: I'm a developer, a community builder, and a relentless learner. I'm a Google Developer Expert in Angular, a Pluralsight author, a conference speaker, and a volunteer for women in tech groups. ❤️🍷📚

Location:
Pacific Northwest USA
Joined:
Mar 12, 2017

Intercepting Http Requests-- Using And Testing Angular's HttpClient

Publish Date: Aug 28 '17
32 17

The HttpClientModule, which debuted in Angular 4.3, is an easy to use API. It automatically expects json as its default response type, builds in the ability to intercept both requests and responses, and makes testing a breeze.

I recently worked on converting calls to use HttpClientModule from HttpModule. While working on this project I had a hard time finding good resources and examples, especially examples of tests. So I decided to write a simple app that uses HttpClientModule along with corresponding unit tests as an example with a little more oomph than what’s available on Angular’s documentation. It’s not perfect as I’m still learning myself, but I hope you’ll find it helpful.

Update

6/10/2018- The code examples in this post and the code in the GitHub repo now uses Angular v6.

Assumptions

This tutorial assumes you have some familiarity with Angular, angular-cli, Material2 libraries, Jasmine, calling REST apis,... The scope of this tutorial is only using the HttpClientModule and the associated unit tests for making http calls.

Background

Using angular-cli, I generated a new application and used Material 2 starter app as a base for the design and for the cute icons. angular-cli was used to generate all the scaffolding for the services, components, and classes. I used JSONPlaceholder API to GET and POST user information. Complete app and code can be found in my GitHub repo.

Creating A User Service

The User Service wraps functionality to create and retrieve user information. This is where we will use HttpClient. In the method getUsers() notice that we no longer have to map the response to json format. We can also declare the type for the response data. I’m using an incredibly unhelpful Array<any> but that’s just to show the syntax.

  public getUsers(): Observable<User[]> {
    return this.http.get<Array<any>>(this.apiEndpoint, {
      headers: new HttpHeaders().set('Accept', 'application/json')
    }).pipe(
      map(this.mapUsers),
      catchError(error => {
        return observableThrowError('An error occurred');
      }),
    );
  }
Enter fullscreen mode Exit fullscreen mode

Don’t forget to add HttpClientModule to app.module.ts imports.

Testing The User Service

Angular made a lot of improvements to testing http calls via the HttpClientTestingModule and HttpClientTestingController. Simply import the HttpClientTestingModule into the TestBed and inject both the HttpClient and HttpTestingController into the tests.

describe('UserService', () => {
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [UserService]
    });
  });

  afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
    httpMock.verify();
  }));
Enter fullscreen mode Exit fullscreen mode

In the test, you mock out a http response and then simulate the http call by flushing TestRequest. You can assert on the http method, expected response, expected headers, and the number of times the http call is made. You’ll want an afterEach method to verify that no more requests remain to be consumed. Here’s a happy path example to verify my response filtering and mapping works.

it('returns users with an id <= 5', inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
      const mockResponse = [
        {
          id: 5,
          name: 'Test5',
          company: {bs: 'blah'}
        },
        {
          id: 6,
          name: 'Test6',
          company: {bs: 'blah'}
        }
      ];

      const userService = getTestBed().get(UserService);
      userService.getUsers().subscribe(
        actualUsers => {
          expect(actualUsers.length).toBe(1);
          expect(actualUsers[0].id).toEqual(5);
        }
      );

      const req = httpMock.expectOne(userService.apiEndpoint);
      expect(req.request.method).toEqual('GET');

      req.flush(mockResponse);
      httpMock.verify();
    }));
Enter fullscreen mode Exit fullscreen mode

Testing the unhappy path isn’t hard at all.

it('should throw with an error message when API returns an error',
      inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {
        const userService = getTestBed().get(UserService);
        userService.getUsers().subscribe({
          error(actualError) {
            expect(of(actualError)).toBeTruthy();
            expect(actualError).not.toBeNull();
            expect(actualError).not.toBeUndefined();
          }
        });

        const req = httpMock.expectOne(userService.apiEndpoint);
        expect(req.request.method).toEqual('GET');

        req.flush({ errorMessage: 'Uh oh!' }, { status: 500, statusText: 'Server Error' });
        httpMock.verify();
      }));
Enter fullscreen mode Exit fullscreen mode

Intercepting Calls

In a real app, we need to authenticate our requests. So we can create our own interceptor by implementing Angular’s HttpInterceptor interface. In this example, I add an Authorization header to each of my calls. I created a fake authentication service to spoof token retrieval and inject the authentication service to better mimic real life and form my token.

export class AuthInterceptor implements HttpInterceptor {
  constructor(private authService: AuthService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  const authReq = req.clone({setHeaders: {Authorization: `${this.authService.tokenType} ${this.authService.tokenValue}`}});
    return next.handle(authReq);
  }
}
Enter fullscreen mode Exit fullscreen mode

In order to use the interceptor, we have to provide it to the app.module.ts.

providers: [UserService, AuthService,
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true
    }],
Enter fullscreen mode Exit fullscreen mode

Now all calls made using HttpClient will include the Authorization header.

Testing Interceptors

Just like before, you need to prep the Testbed. But this time we’re also going to provide the interceptor. I’m also providing a mock to the authentication service.

beforeEach(() => {
        TestBed.configureTestingModule({
            imports: [HttpClientTestingModule],
            providers: [
                {
                    provide: AuthService,
                    useValue: mockAuthService
                },
                {
                    provide: HTTP_INTERCEPTORS,
                    useClass: AuthInterceptor,
                    multi: true
                }]
        });
    });
Enter fullscreen mode Exit fullscreen mode

Testing the interceptor is pretty much the same as testing the User Service. Except in this case we’ll customize the verification to check for the header.

describe('making http calls', () => {
        it('adds Authorization header', inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => {

            http.get('/data').subscribe(
                response => {
                    expect(response).toBeTruthy();
                }
            );

            const req = httpMock.expectOne(r =>
                r.headers.has('Authorization') &&
                r.headers.get('Authorization') === `${mockAuthService.tokenType} ${mockAuthService.tokenValue}`);
            expect(req.request.method).toEqual('GET');

            req.flush({ hello: 'world' });
            httpMock.verify();
        }));
    });
Enter fullscreen mode Exit fullscreen mode

That’s all I have for now. I hope you found this useful. All my code can be found in my GitHub repo. Please feel free to check it out and I’m open to any feedback you have.

Comments 17 total

  • Doug Seelinger
    Doug SeelingerAug 31, 2017

    How timely! Need this info for a current customer and besides the Angular docs (which is pretty thin on this particular topic), there's not much else out there on this. You'd think everyone would need to test their services that use the HttpClient.

    • Alisa
      AlisaAug 31, 2017

      Thanks! I agree with Angular docs being thin on testing. There's some info out there and I can look at Angular's repos for guidance but there's not enough examples of tests for applications. Angular's tests are specific to the framework functionality.

      Good luck on your project!

  • Neil Menzies
    Neil MenziesJan 5, 2018

    Thanks for the article - wanted to unit test an HttpInterceptor, and this provided the perfect template.

    • Alisa
      AlisaFeb 20, 2019

      Thank you for your kind words!

  • Hussein Koprly
    Hussein KoprlyMar 27, 2018

    Great explanation,
    I was trying to test my interceptor and I'm acquiring an access token in the interceptor and attach it to the request like this:
    `return authService.acquireToken(source).mergeMap(//somecode);
    I'm having problem with mergeMap, I'm not sure how to mock this part ?
    Any idea ?

    • Alisa
      AlisaMar 28, 2018

      Thanks, I'm glad you liked it.

      I'm assuming you are mocking the authService's acquireToken method. If so, you shouldn't need to mock mergeMap.

      I don't know your use case, but if the mergeMap is complicated, should it be part of acquireToken (or pulled out differently) so that it can be tested independently? It sounds like you might have a lot going on in the interceptor.

      Best wishes on your project!

      • Hussein Koprly
        Hussein KoprlyMar 28, 2018

        Thank you Alisa,

        yes, I'm trying to mock the auth service which calls acquireToken method that is part of the adal.js library. Because of the method returns an Observable, so I'm chaining mergeMap to acquireToken in order to wait to get the token.

        I will try to take it out of the interceptor to make the test easier.

        Thanks again

  • EI Akoji
    EI AkojiMay 14, 2018

    Great post Alisa! I had a hard time working with services that make http request. Your solution is a very good starting point. For services that make multiple http request, I am taking a look at - angular.io/guide/http#handling-mor.... Do you have anything on this?

  • Mohit
    MohitAug 31, 2018

    My interceptor is like:

    return next.handle(request).catch((error) => {
    ...
    ...
    ...
    return Observable.throw(error);
    }

    in the service test I am unable to assert this error. Any advise?

  • G. Más
    G. MásFeb 20, 2019

    Thank you so much! This is one of the most helpful articles I found on this topic. Super clear!

    • Alisa
      AlisaFeb 20, 2019

      Thank you! I'm glad you found it helpful!

  • Markus Rossler
    Markus RosslerApr 30, 2019

    I would like to change a successful HttpResponse to an simulated error, returning an HttpErrorResponse. Any hint? I already tried mapping the event to an error. No success...

    • Alisa
      AlisaMay 9, 2019

      Are you trying to simulate an error response in the service or in the interceptor (for intercepting responses)?

      • Markus Rossler
        Markus RosslerMay 9, 2019

        Our server api has some special behaviour, in some cases it replies with an http error instead of a http 200 response with a proper body. This is legacy code, but on purpose. Not to break anything we need to handle with that. Since these are rare cases i want to simulate that and reuse it for unit tests as well.

  • Yann Bertrand
    Yann BertrandNov 20, 2019

    Thanks, Alisa, for that article.

    I really don't like the way we have to do that in Angular. For those who didn't try it out, let me explain what happen in terms of timeline:

    • .subscribe() launch a request and give a callback to be called once we get the response.
    • .expectOne() verify that an endpoint has been called
    • .flush() respond to that request
    • the subscribe callback gets called, and we can finally verify that we have a correct answer.

    By the way, the order of the code is really important, if you move just one line of it, the test breaks.

    In my sense, we should be able to write it like:

    • describe that when a request to /whatever is sent, we pass a mock answer (or throw if that's what we want to test)
    • launch the request
    • verify the answer

    That would be way easier to explain to people and let us put our mocked request => response in a beforeEach!

    How do you feel about it?

    • Alisa
      AlisaMar 2, 2020

      Hi there! Sorry it took me so long to respond; I didn't see this comment until now. 😬

      I hear what you're saying, it is a lot of steps and I've made the mistake of not calling .flush() when testing myself. However, I do like that Angular's HttpClientTestingModule allows me to verify expected number of times a call is made without using a Jasmine spy. I also like that I can verify the HTTP method used in the call, headers, etc separately from verifying the subscribe portion.

      For more complicated service calls (where there's retries, different HTTP methods, or custom headers), being able to assert separately starts making more sense.

      For straightforward requests like what I have in this post, having an all-in-one solution like what you propose is a great way to go and definitely helps to make things easier. 🙂

      Thank you for taking the time to read and comment! These kinds of discussions are great!

Add comment