Angular 11:对 HttpInterceptor 进行单元测试 - 异步计时器或 AfterAll 错误

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

我正在努力让我的 HttpInterceptor 通过我的单元测试,但我 运行 遇到了让测试通过的问题。我有 6 个测试,如果我 运行 测试作为“正常”测试,当测试在本地完成时,我经常得到“A error was throwed in afterAll”,但我的 Azure 管道每次都失败并出现相同的错误。

如果我 运行 使用 fakeAsync() 在异步区域中进行测试,每个测试都会出现“1 个计时器仍在队列中”错误。

这是拦截器的代码:

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);
  }
}

这是我的规范文件:

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();
  }));

});

似乎有一个异步调用没有完成。我在 httpClient.get() 调用后尝试使用 flush(),我尝试将其放在 request = httpMock.expectOne('/api') 之后,每次调用后似乎都无法真正修复错误。

这也可能是我笨拙的每个条件一个测试的方法。

非常欢迎好的想法和反馈!

谢谢,Bjarne

因此,在对 HttpTestingController 进行一些研究后,我能够重新创建并重现错误。解法和我之前的答案相差不远,但完全不正确。

subscribe()之后少了一个tick(200),但是由于返回错误,您必须在订阅中处理错误。

所以这里是第一个测试的工作示例。

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();
  }));