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

如何测试带重试策略的Angular 18错误拦截器?

Angular 18错误拦截器单元测试方案

问题背景

我实现了一个Angular 18错误拦截器,能够对请求错误进行重试以排除偶发问题,功能正常,但编写单元测试时无法正确验证重试逻辑:需要验证500错误会触发2次请求且第二次带延迟,同时401、403、503错误不会触发重试。

拦截器代码

let apiService: ApiService;

const genericRetryStrategy = (): ((e: HttpErrorResponse, i: number) => Observable<number>) => {
  const maxRetryAttempts = 1;
  const scalingDuration = 200;
  const excludedStatusCodes = [401, 403, 503];
  return (error: HttpErrorResponse, i: number): Observable<number> => {
    const retryAttempt = i + 1;
    if (retryAttempt > maxRetryAttempts || excludedStatusCodes.some((code) => code === error.status)) {
      return throwError(() => error);
    }
    console.info(`Attempt ${retryAttempt.toString()}: retrying in ${(retryAttempt * scalingDuration).toString()}ms`);
    return timer(retryAttempt * scalingDuration);
  };
};

const catchAll$ = (code: string, url: string, error: HttpErrorResponse): Observable<never> => {
  console.error('Logging Error', code, error.message);
  apiService.logError$(msg, error.error, url).subscribe({
    error: () => {
      console.error('Unable to log error!');
    },
  });

  return throwError(() => new Error(msg));
};

export const httpErrorInterceptor: HttpInterceptorFn = (req, next) => {
  apiService = inject(ApiService);

  return next(req).pipe(
    retry({ delay: genericRetryStrategy() }),
    catchError((error: HttpErrorResponse) => {
      return catchAll$(code, req.url, error);
    }),
  );
};

现有测试代码

const interceptor: HttpInterceptorFn = (req, next) => TestBed.runInInjectionContext(() => httpErrorInterceptor(req, next));
let httpClient: HttpClient;
let httpMock: HttpTestingController;

beforeEach(() => {
  TestBed.configureTestingModule({
    imports: [],
    providers: [
      provideHttpClient(withInterceptors([interceptor])),
      provideHttpClientTesting(),
      {
        provide: ApiService,
        useValue: createSpyFromClass(ApiService, {
          methodsToSpyOn: ['logError$'],
        }),
      },
    ],
  });

  httpClient = TestBed.inject(HttpClient);
  httpMock = TestBed.inject(HttpTestingController);
});

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

describe('Error Retrying', () => {
  it('should retry a 500 error only once with a slight delay', fakeAsync(() => {
      httpClient.get('http://server.biz/api/whatever').subscribe({
        next: () => {
          throw new Error('This should not have been successful!');
        },
        error: (err) => {
          console.log('error 1', err);
        },
      });
      const req1 = httpMock.expectOne('http://server.biz/api/whatever');
      req1.flush({}, { status: 500, statusText: 'Server Error 1' });
      tick(650);

      expect(console.info).toHaveBeenCalledWith(`Attempt 1: retrying in 200ms`);
      expect(console.info).toHaveBeenCalledWith(`Attempt 2: retrying in 400ms`);

      // ???
    }));

  it('should NOT retry a 401 error', () => {
    httpClient.get('http://example.com/api/whatever').subscribe();
    const req1 = httpMock.expectOne('http://server.biz/api/whatever');
    req1.flush('message here', { status: 401, statusText: 'Unauthorized' });

    // ???
  });

  it('should NOT retry a 403 error', () => {
    // ???
  });

  it('should NOT retry a 503 error', () => {
    // ???
  });
});

测试完善方案

1. 验证500错误重试一次(共2次请求)且带延迟

通过控制时间流捕获重试请求,同时验证日志输出:

it('should retry a 500 error only once with a slight delay', fakeAsync(() => {
  // 监听console.info,用于验证重试日志
  spyOn(console, 'info');

  httpClient.get('http://server.biz/api/whatever').subscribe({
    next: () => fail('请求不应成功'),
    error: () => {}
  });

  // 捕获并响应第一次请求
  const firstReq = httpMock.expectOne('http://server.biz/api/whatever');
  firstReq.flush({}, { status: 500, statusText: 'Server Error' });

  // 验证第一次重试的日志输出
  expect(console.info).toHaveBeenCalledWith(`Attempt 1: retrying in 200ms`);

  // 快进200ms,触发重试逻辑
  tick(200);

  // 捕获第二次请求并响应500
  const secondReq = httpMock.expectOne('http://server.biz/api/whatever');
  secondReq.flush({}, { status: 500, statusText: 'Server Error' });

  // 快进足够时间,确认没有更多重试
  tick(400);
  httpMock.expectNone('http://server.biz/api/whatever');

  // 确认重试日志只触发一次(最多重试1次)
  expect(console.info).toHaveBeenCalledTimes(1);
}));

2. 验证401错误不触发重试

通过expectNone确认没有重试请求:

it('should NOT retry a 401 error', () => {
  const testUrl = 'http://server.biz/api/whatever';
  httpClient.get(testUrl).subscribe({
    error: () => {}
  });

  const req = httpMock.expectOne(testUrl);
  req.flush('Unauthorized', { status: 401, statusText: 'Unauthorized' });

  // 确认没有重试请求发出
  httpMock.expectNone(testUrl);
});

3. 验证403错误不触发重试

复用401测试的核心逻辑:

it('should NOT retry a 403 error', () => {
  const testUrl = 'http://server.biz/api/whatever';
  httpClient.get(testUrl).subscribe({
    error: () => {}
  });

  const req = httpMock.expectOne(testUrl);
  req.flush('Forbidden', { status: 403, statusText: 'Forbidden' });

  httpMock.expectNone(testUrl);
});

4. 验证503错误不触发重试

同样验证无重试请求:

it('should NOT retry a 503 error', () => {
  const testUrl = 'http://server.biz/api/whatever';
  httpClient.get(testUrl).subscribe({
    error: () => {}
  });

  const req = httpMock.expectOne(testUrl);
  req.flush('Service Unavailable', { status: 503, statusText: 'Service Unavailable' });

  httpMock.expectNone(testUrl);
});

额外注意事项

  • 修正测试中URL不一致的问题(原测试存在http://example.com和http://server.biz混用的情况),确保请求URL统一。
  • 使用fail('请求不应成功')替代手动抛出错误,更符合Jasmine测试规范。
  • httpMock.verify()会在afterEach中自动校验是否有未处理的请求,保证测试用例的干净性。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 23:32:02