Bjarne B. Dollerup
Bjarne B. Dollerup

Reputation: 87

Angular 11: Unit testing an HttpInterceptor - async timer or AfterAll errors

I'm working on getting my HttpInterceptor to pass my unit tests and I have run into issues with getting the tests to pass. I have 6 tests and If I run the tests as "normal" tests, I often get "An error was thrown in afterAll" as the tests finish locally, but my Azure pipelines fail with the same error every time.

If I run the tests in an async zone using fakeAsync(), I get a "1 timer(s) still in the queue" error for each.

Here's the code for the Interceptor:

import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, throwError } from 'rxjs';
import { retry, tap, catchError } from 'rxjs/operators';
import { HttpServiceError } from '../models/httpServiceError.model';
import { LoggingService, LogLevel } from '../services/logging.service';

@Injectable({
    providedIn: 'root'
})

export class HttpResponseInterceptorService implements HttpInterceptor{
  public logLevel!: LogLevel;

  constructor(private readonly loggingService: LoggingService) {
// Intentionally left blank
  }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request)
      .pipe(
        retry(1),
        tap(() => console.log('ResponseInterceptor called')),
        catchError((error: HttpErrorResponse) => {
          this.handleHttpError(error);
          return throwError(error);
        }));
  }

  private handleHttpError(error: HttpErrorResponse): Observable<HttpServiceError> {
    const requestError = new HttpServiceError();    
    
    requestError.errorNumber = error.status;
    requestError.statusMessage = error.statusText;

    switch (error.status) {
      case 401: {
        requestError.friendlyMessage = 'You are not logged in';
        break;
      }
      case 403: {
        requestError.friendlyMessage = 'You are not logged in';
        break;
      }
      case 404: {
        requestError.friendlyMessage = 'The service failed to respond';
        break;
      }
      case 429: {
        requestError.friendlyMessage = 'The service is busy';
        break;
      }
      case 500: {
        requestError.friendlyMessage = 'The service is not responding correctly';
        break;
      }
      default: {
        requestError.friendlyMessage = 'An error occured retrieving the data';
        break;
      }
    }

    // TODO replace with some kind of notification
    // window.alert(errorMessage);

    this.loggingService.logWithLevel(JSON.stringify(requestError), LogLevel.Information);

    return throwError(requestError);
  }
}

Here's my spec file:

import { HttpClient, HttpErrorResponse, HTTP_INTERCEPTORS } from '@angular/common/http';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { fakeAsync, flush, inject, TestBed } from '@angular/core/testing';
import { LoggingService } from '../services/logging.service';

import { HttpResponseInterceptorService } from './httpresponseinterceptor.service';

describe('HttpResponseInterceptorService', () => {
  let interceptor: HttpResponseInterceptorService;
  let httpMock: HttpTestingController;
  let loggingService: LoggingService;
  let httpClient: HttpClient;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [
        {
          provide: HTTP_INTERCEPTORS,
          useClass: HttpResponseInterceptorService,
          multi: true,
        },
      ]
    });
    loggingService = TestBed.inject(LoggingService);
    interceptor = TestBed.inject(HttpResponseInterceptorService);
    httpMock = TestBed.inject(HttpTestingController);
    httpClient = TestBed.inject(HttpClient);
  });

  afterEach(() => {
    httpMock.verify();
  });

  afterAll(() => {
    TestBed.resetTestingModule();
  });

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

  it('should call loggingService with a friendlyMessage of An error occured retrieving the data (everything else)', fakeAsync(() => {
    spyOn(loggingService,'logWithLevel');

    const errorResponse = new HttpErrorResponse({
      error: '599 error',
      status: 599, statusText: 'Unknown Error'
    });

    httpClient.get('/api').subscribe();

    let request = httpMock.expectOne('/api');
    request.error(new ErrorEvent('599 First Unknown Error'), errorResponse);

    request = httpMock.expectOne('/api');
    request.error(new ErrorEvent('599 Second Unknown Error'), errorResponse);

    expect(loggingService.logWithLevel).toHaveBeenCalled();
  }));

  it('should call loggingService with a friendlyMessage of You are not logged in (401)', fakeAsync(() => {
    spyOn(loggingService,'logWithLevel');

    const errorResponse = new HttpErrorResponse({
      error: '401 error',
      status: 401, statusText: 'Unauthorized'
    });

    httpClient.get('/api').subscribe();

    let request = httpMock.expectOne('/api');
    request.error(new ErrorEvent('401 First Unauthorized'), errorResponse);

    request = httpMock.expectOne('/api');
    request.error(new ErrorEvent('401 Second Unauthorized'), errorResponse);

    expect(loggingService.logWithLevel).toHaveBeenCalled();
  }));

  it('should call loggingService with a friendlyMessage of You are not logged in (403)', fakeAsync(() => {
    spyOn(loggingService,'logWithLevel');

    const errorResponse = new HttpErrorResponse({
      error: '403 error',
      status: 403, statusText: 'Forbidden'
    });

    httpClient.get('/api').subscribe();

    let request = httpMock.expectOne('/api');
    request.error(new ErrorEvent('403 First Forbidden'), errorResponse);

    request = httpMock.expectOne('/api');
    request.error(new ErrorEvent('403 Second Forbidden'), errorResponse);

    expect(loggingService.logWithLevel).toHaveBeenCalled();
  }));

  it('should call loggingService with a friendlyMessage of The service failed to respond (404)', fakeAsync(() => {
    spyOn(loggingService,'logWithLevel');

    const errorResponse = new HttpErrorResponse({
      error: '404 error',
      status: 404, statusText: 'Not Found'
    });

    httpClient.get('/api').subscribe();

    let request = httpMock.expectOne('/api');
    request.error(new ErrorEvent('404 First Error'), errorResponse);

    request = httpMock.expectOne('/api');
    request.error(new ErrorEvent('404 Second Error'), errorResponse);

    expect(loggingService.logWithLevel).toHaveBeenCalled();
  }));

  it('should call loggingService with a friendlyMessage of The service is busy (429)', fakeAsync(() => {
    spyOn(loggingService,'logWithLevel');

    const errorResponse = new HttpErrorResponse({
      error: '429 error',
      status: 429, statusText: 'Too Many Requests'
    });

    httpClient.get('/api').subscribe();

    let request = httpMock.expectOne('/api');
    request.error(new ErrorEvent('429 First Too Many Requests'), errorResponse);

    request = httpMock.expectOne('/api');
    request.error(new ErrorEvent('429 Second Too Many Requests'), errorResponse);

    expect(loggingService.logWithLevel).toHaveBeenCalled();

  }));

  it('should call loggingService with a friendlyMessage of The service is not responding correctly (500)', fakeAsync(() => {
    spyOn(loggingService,'logWithLevel');

    const errorResponse = new HttpErrorResponse({
      error: '500 error',
      status: 500, statusText: 'Internal Server Error'
    });

    httpClient.get('/api').subscribe();

    let request = httpMock.expectOne('/api');
    request.error(new ErrorEvent('500 First Internal Server Error'), errorResponse);

    request = httpMock.expectOne('/api');
    request.error(new ErrorEvent('500 Second Internal Server Error'), errorResponse);

    expect(loggingService.logWithLevel).toHaveBeenCalled();
  }));

});

It seems that there'a an async call that doesn't get completed. I have tried with flush() after the httpClient.get() call, I have tried to put it after both request = httpMock.expectOne('/api'), after each call and nothing seems to actually fix the error.

It may also be down to my clumsy one-test-per-condition approach.

Good ideas and feedback are very welcomed!

Thanks, Bjarne

Upvotes: 1

Views: 911

Answers (1)

Ander2
Ander2

Reputation: 5658

So, after some research on the HttpTestingController I was able to recreate and reproduce the error. The solution is not far away from my previous answer, but it not was correct at all.

There is a tick(200) missing after the subscribe(), but as you are returning an error, you have to handle the error in the subscription.

So here you have a working example of the first test.

fit("should call loggingService with a friendlyMessage of An error occured retrieving the data (everything else)", fakeAsync(() => {
    spyOn(loggingService, "logWithLevel");

    const errorResponse = new HttpErrorResponse({
      error: "599 error",
      status: 599,
      statusText: "Unknown Error"
    });

    httpClient
      .get("/api")
      .subscribe(_ => console.log("Good"), err => console.log(err));

    let request = httpMock.expectOne("/api");
    request.error(new ErrorEvent("599 First Unknown Error"), errorResponse);

    request = httpMock.expectOne("/api");
    request.error(new ErrorEvent("599 Second Unknown Error"), errorResponse);

    tick(100);
    expect(loggingService.logWithLevel).toHaveBeenCalled();
  }));

Upvotes: 1

Related Questions