Search code examples
angularunit-testingjestjsangular-httpclient-interceptors

HttpClient interceptor retry unit testing (Expected no open requests, found 1)


I have a simple HttpClient interceptor that just retries failed requests a set amount of times. Now, I'm trying to write unit tests for it but somehow I end up with an unfinished request. I've tried wrapping the test in fakeAsync or using done but it does not seem to help. Right now I am out of ideas.

The error I get is: Error: Expected no open requests, found 1: GET mock/url

What am I missing?

server-error.interceptor.ts

import { Injectable } from '@angular/core';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
import { retry } from 'rxjs/operators';
import { ConfigService } from '@app/shared/services/config.service';

/**
 * Http error interceptor class
 */

@Injectable()
export class ServerErrorInterceptor implements HttpInterceptor {
  constructor(private configService: ConfigService) {}

  /**
   * Intercepts failed requests and retries set amount of times before throwing an error
   *
   * @param request  HTTP request
   * @param next  HTTP handler
   *
   * @returns Observable of Http event
   */
  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(retry(this.configService.config.httpRetryCount));
  }
}

server-error.interceptor.spec.ts

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController, TestRequest } from '@angular/common/http/testing';
import { HTTP_INTERCEPTORS, HttpClient, HttpErrorResponse } from '@angular/common/http';

import { ServerErrorInterceptor } from '@core/interceptors/server-error.interceptor';
import { ConfigService } from '@shared/services/config.service';

const mockUrl = 'mock/url';
const mockHttpRetryCount = 5;
const mockErrorText = 'Mock error!';

const mockConfigService: object = {
  config: jest.fn().mockReturnValue({
    httpRetryCount: mockHttpRetryCount
  })
};

describe('ServerErrorInterceptor', () => {
  let httpClient: HttpClient;
  let httpTestingController: HttpTestingController;
  let interceptor: ServerErrorInterceptor;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [
        ServerErrorInterceptor,
        { provide: ConfigService, useValue: mockConfigService },
        { provide: HTTP_INTERCEPTORS, useClass: ServerErrorInterceptor, multi: true }
      ]
    });

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

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

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

  it('should retry an errored request set amount of times', (): void => {
    httpClient.get(mockUrl).subscribe(
      null,
      (response: HttpErrorResponse): void => {
        expect(response.status).toEqual(500);
        expect(response.message).toEqual(`Http failure response for ${mockUrl}: 500 Server Error`);
      }
    );

    for (let i = 0; i < mockHttpRetryCount; i += 1) {
      const request: TestRequest = httpTestingController.expectOne(mockUrl);
      request.flush(mockErrorText, { status: 500, statusText: 'Server Error' });
    }
  });
});


Solution

  • I've actually found an answer myself. There were 3 separate bugs in my code:

    1. The ConfigService was mocked the wrong way, and was returning undefined for httpRetryCount
    2. The interceptor wasn't throwing an error after all retries failed.
    3. The for loop in the test should be fired mockHttpRetryCount + 1 to account for all the retries and then the final result (either positive or negative)