如何测试带重试策略的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
相关产品推荐
相关产品推荐

