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

如何用Jest测试Angular中带重试策略的HttpInterceptor

解决HttpErrorInterceptor重试逻辑的单元测试问题

一、修正HttpTestingController测试的TypeError问题

TypeError大多是因为重试过程中发起了多次HTTP请求,而你误用了仅匹配单次请求的expectOne,导致后续重试请求找不到匹配项抛出错误。正确做法是根据重试次数,多次调用expectOne并依次处理响应;如果重试有延迟,需用fakeAsync+tick模拟时间流逝。

测试1:所有重试返回500,最终返回错误

import { TestBed } from '@angular/core/testing';
import { HttpClient, HTTP_INTERCEPTORS } from '@angular/common/http';
import { HttpErrorInterceptor } from './http-error.interceptor';
import { HttpTestingController, HttpClientTestingModule } from '@angular/common/http/testing';

describe('HttpErrorInterceptor', () => {
  let httpClient: HttpClient;
  let httpTestingController: HttpTestingController;
  const RETRY_TIMES = 3; // 对应你的重试策略次数

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

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

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

  it('should return error when all retries fail with 500', (done) => {
    httpClient.get('/test').subscribe({
      next: () => fail('Expected error, not success'),
      error: (err) => {
        expect(err.status).toBe(500);
        done();
      }
    });

    // 处理初始请求+所有重试请求
    for (let i = 0; i <= RETRY_TIMES; i++) {
      const req = httpTestingController.expectOne('/test');
      req.flush('Server error', { status: 500, statusText: 'Internal Server Error' });
    }
  });

测试2:某次重试成功,返回结果

it('should return success when retry succeeds', (done) => {
    httpClient.get('/test').subscribe({
      next: (res) => {
        expect(res).toBe('Success');
        done();
      },
      error: () => fail('Expected success, not error')
    });

    // 前两次返回500,第三次返回成功(对应重试2次)
    const firstReq = httpTestingController.expectOne('/test');
    firstReq.flush('Error', { status: 500, statusText: 'Internal Server Error' });

    const secondReq = httpTestingController.expectOne('/test');
    secondReq.flush('Error', { status: 500, statusText: 'Internal Server Error' });

    const thirdReq = httpTestingController.expectOne('/test');
    thirdReq.flush('Success', { status: 200, statusText: 'OK' });
  });
});

如果重试策略包含延迟,需用fakeAsync模拟时间:

it('should handle retries with delay', fakeAsync(() => {
  let errorReceived = false;
  httpClient.get('/test').subscribe({
    error: () => errorReceived = true
  });

  for (let i = 0; i <= RETRY_TIMES; i++) {
    const req = httpTestingController.expectOne('/test');
    req.flush('Error', { status: 500 });
    tick(1000); // 匹配你的重试间隔时间
  }

  expect(errorReceived).toBe(true);
}));

二、修复RxJS Marble测试重试未触发的问题

Marble测试必须用TestScheduler控制RxJS时间调度,确保重试的延迟逻辑被正确模拟,所有流操作要放在TestScheduler.run回调中执行。

假设你的重试策略基于retryWhen实现,对应的Marble测试代码:

import { TestScheduler } from 'rxjs/testing';
import { HttpErrorInterceptor } from './http-error.interceptor';
import { HttpRequest, HttpHandler, HttpResponse, HttpErrorResponse } from '@angular/common/http';
import { of, throwError } from 'rxjs';

describe('HttpErrorInterceptor (Marble Tests)', () => {
  let testScheduler: TestScheduler;
  let interceptor: HttpErrorInterceptor;
  let httpHandler: jasmine.SpyObj<HttpHandler>;

  beforeEach(() => {
    testScheduler = new TestScheduler((actual, expected) => {
      expect(actual).toEqual(expected);
    });
    interceptor = new HttpErrorInterceptor();
    httpHandler = jasmine.createSpyObj('HttpHandler', ['handle']);
  });

  it('should return error when all retries fail', () => {
    testScheduler.run(({ expectObservable }) => {
      const request = new HttpRequest('GET', '/test');
      // 模拟3次请求都返回500
      httpHandler.handle.and.returnValues(
        throwError(() => new HttpErrorResponse({ status: 500 })),
        throwError(() => new HttpErrorResponse({ status: 500 })),
        throwError(() => new HttpErrorResponse({ status: 500 }))
      );

      const result$ = interceptor.intercept(request, httpHandler);
      // ---# 表示延迟后抛出错误(#代表错误事件)
      expectObservable(result$).toBe('---#', undefined, new HttpErrorResponse({ status: 500 }));
    });
  });

  it('should return success when retry succeeds', () => {
    testScheduler.run(({ expectObservable }) => {
      const request = new HttpRequest('GET', '/test');
      // 前两次返回500,第三次返回成功
      httpHandler.handle.and.returnValues(
        throwError(() => new HttpErrorResponse({ status: 500 })),
        throwError(() => new HttpErrorResponse({ status: 500 })),
        of(new HttpResponse({ body: 'Success', status: 200 }))
      );

      const result$ = interceptor.intercept(request, httpHandler);
      // -------x 表示经过两次延迟后发出成功值(x代表next事件)
      expectObservable(result$).toBe('-------x', { x: 'Success' });
    });
  });
});

关键注意事项

  1. 所有流操作必须包裹在TestScheduler.run中,确保时间被调度器管控。
  2. Marble字符串的长度要对应重试延迟时间(比如每个-代表10ms,1秒延迟对应100-)。
  3. httpHandler.handle的调用次数要和重试次数匹配,每次返回对应的错误或成功响应。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 11:53:18