You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular HttpInterceptor单元测试:toHaveBeenCalledWith提示未调用

问题:HttpInterceptor单元测试中无法验证handleError的调用情况

我在对一个错误处理HttpInterceptor做单元测试,想验证handleError函数是否携带指定参数被调用,但控制台里toHaveBeenCalledWith断言一直提示“该函数从未被调用”,其他测试都正常运行。


Error.interceptor.ts代码

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
  constructor() {}

  handleError(error: HttpErrorResponse): Observable<any> {
    let errorMsg = '';
    if (error.status === HTTP_STATUS_ABORTED) {
      errorMsg = 'An client-side or network error occurred';
    } else if (error.status === HttpStatusCode.InternalServerError) {
      errorMsg = 'An internal server error occurred';
    } else {
      errorMsg = `Backend returned code ${error.status}`;
    }

    console.error(errorMsg, ', body was: ', error.error);

    // Return an observable with a user-facing error message.
    return throwError(() => {
      return new Error(errorMsg);
      // return error;
    });
  }

  intercept(
    request: HttpRequest<unknown>,
    next: HttpHandler
  ): Observable<HttpEvent<unknown>> {
    return next.handle(request).pipe(catchError(this.handleError));
  }
}

Error.interceptor.spec.ts代码

describe('ErrorInterceptor', () => {
  let client: HttpClient;
  let httpController: HttpTestingController;
  let interceptor: ErrorInterceptor;

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

    client = TestBed.inject(HttpClient);
    httpController = TestBed.inject(HttpTestingController);
    interceptor = TestBed.inject(ErrorInterceptor);
    spyOn(console, 'error');
  });

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

  it('should call handleError with the correct errorObject on code 400', () => {
    spyOn(interceptor, 'handleError').and.callThrough();

    const expectedErrorResponse = new HttpErrorResponse({
      url: '/target',
      status: HttpStatusCode.BadRequest,
      statusText: 'Bad Request',
      error: new ProgressEvent('ERROR', {}),
    });

    client.get('/target').subscribe({
      error: (error: Error) => {
        expect(error).toBeTruthy();
        expect(error).toEqual(new Error('Backend returned code 400'));

        expect(console.error).toHaveBeenCalledWith(
          'Backend returned code 400',
          ', body was: ',
          expectedErrorResponse.error
        );

        expect(interceptor.handleError).toHaveBeenCalledWith(
          expectedErrorResponse
        );
      },
    });

    const httpRequest: HttpRequest<any> = new HttpRequest('GET', '/target');
    const err = new ProgressEvent('ERROR', {});

    httpController.expectOne(httpRequest).error(err, {
      status: HttpStatusCode.BadRequest,
      statusText: 'Bad Request',
    });
  });

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

我期望通过expect(interceptor.handleError).toHaveBeenCalledWith(expectedErrorResponse);验证函数被正确调用,但未达到预期。


修复方案

  • 在测试的subscribe回调中调用done函数
    异步测试需要调用done()通知测试框架异步操作已完成,避免断言在异步回调执行前就被判定失败:

    it('should call handleError with the correct errorObject on code 400', (done: DoneFn) => {
        const spyOnHandleError = spyOn(
          interceptor,
          'handleError'
        ).and.callThrough();
    
        const expectedErrorResponse = new HttpErrorResponse({
          url: '/target',
          status: HttpStatusCode.BadRequest,
          statusText: 'Bad Request',
          error: new ProgressEvent('ERROR', {}),
        });
    
        client.get('/target').subscribe({
          next: (returnValue) => {
            fail(`Expected error but got "${returnValue}"`);
          },
    
          error: (error: Error) => {
            expect(error).toBeTruthy();
            expect(error).toEqual(new Error('Backend returned code 400'));
    
            expect(interceptor.handleError).toHaveBeenCalledWith(
              expectedErrorResponse
            );
    
            done();
          },
    
          complete: () => fail('Complete must not be called'),
        });
    
        const httpRequest: HttpRequest<any> = new HttpRequest('GET', '/target');
        const err = new ProgressEvent('ERROR', {});
    
        httpController.expectOne(httpRequest).error(err, {
          status: HttpStatusCode.BadRequest,
          statusText: 'Bad Request',
        });
      });
    
  • 确保Provider使用同一实例
    原来的配置会创建两个ErrorInterceptor实例,测试中spy的实例和实际拦截器使用的实例不一致,改用useExisting复用已注入的实例:

    providers: [
      ErrorInterceptor, // 实例A
      {
        provide: HTTP_INTERCEPTORS,
        useExisting: ErrorInterceptor, // 复用实例A
        multi: true,
      },
    ],
    
  • 修改interceptor中的catchError调用方式
    直接传递this.handleError会丢失this上下文,改用箭头函数绑定正确的上下文:

    .pipe(catchError((err) => this.handleError(err)));
    

内容的提问来源于stack exchange,提问作者Harald

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 20:35:13