Angular HTTP拦截器单元测试:如何监视getNewTokenAndRetryRequest方法?
我正在对用于授权的Angular HTTP拦截器进行单元测试,已覆盖基础成功场景,现在要测试访问令牌过期导致API调用失败的场景。但运行测试时出现错误:Error: Expected spy getNewTokenAndRetryRequest to have been called.,求正确监视AuthInterceptorService的getNewTokenAndRetryRequest()方法的方案。
我的单元测试代码
beforeEach(() => { TestBed.configureTestingModule({ imports: [ HttpClientTestingModule ], providers: [TokenStorageService, { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptorService, multi: true } ] }); service = TestBed.inject(AuthInterceptorService); }); it('should proceed to refresh token logic on 500 error', inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => { let thownError: Error = null; spyOn(service, "getNewTokenAndRetryRequest").and.stub(); Object.defineProperty(window.document, 'cookie', { writable: true, value: 'access_token=mycookie', }); http.get('/data').subscribe( response => { expect(response).toBeTruthy(); } ); const req = httpMock.expectOne(r => r.headers.has('Authorization') && r.headers.get('Authorization').startsWith('Bearer ewogIC') ); req.flush("failure",{ status: 500, statusText: "500 error" }); expect(service.getNewTokenAndRetryRequest).toHaveBeenCalled(); httpMock.verify(); }));
我的拦截器代码
export class AuthInterceptorService implements HttpInterceptor { constructor(private tokenStorageService: TokenStorageService, private httpClient: HttpClient) { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<any> { const updatedHeaders:HttpHeaders = req.headers.set('Authorization', 'Bearer ' + this.tokenStorageService.getAccessToken()); const updatedRequest = req.clone({headers: updatedHeaders}) return next.handle(updatedRequest) .pipe( catchError((error: HttpErrorResponse) => { if (error.status===500) { console.log('500 error, try to use the refresh token'); try{ this.getNewTokenAndRetryRequest(req); return of([]); }catch (error) { return throwError(error); } }else{ let errorMsg = `Error Code: ${error.status}, Message: ${error.message}`; return throwError(error.error); } }) ) } getNewTokenAndRetryRequest(req: HttpRequest<any>): void { //do stuff } }
问题分析与解决方法
1. 拦截器实例不匹配
你通过TestBed.inject(AuthInterceptorService)获取的实例,和Angular拦截器链中实际运行的实例并非同一个。因为用useClass提供拦截器时,Angular会创建新实例,导致你监视的实例从未被调用。
**解决:**手动创建拦截器实例并通过useValue提供,确保测试使用同一个实例:
beforeEach(() => { // 手动创建拦截器实例 const authInterceptor = new AuthInterceptorService( TestBed.inject(TokenStorageService), TestBed.inject(HttpClient) ); TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ TokenStorageService, { provide: HTTP_INTERCEPTORS, useValue: authInterceptor, // 替换useClass为useValue multi: true } ] }); service = authInterceptor; // 直接使用该实例 });
2. 请求匹配条件错误
你的expectOne要求请求头以Bearer ewogIC开头,但实际头是Bearer 加上tokenStorageService.getAccessToken()的返回值。如果getAccessToken()返回的不是ewogIC...,就会匹配失败,后续的req.flush不会执行,拦截器的错误分支也不会触发。
解决:
- 简化匹配条件,只检查Authorization头存在:
const req = httpMock.expectOne(r => r.headers.has('Authorization'));
- 或者Mock TokenStorageService的
getAccessToken方法,返回你期望的字符串:
const tokenStorage = TestBed.inject(TokenStorageService); spyOn(tokenStorage, 'getAccessToken').and.returnValue('ewogIC...');
3. 异步操作未等待
拦截器的错误处理是异步的,直接断言可能会在操作完成前执行,导致断言失败。
**解决:**使用fakeAsync和tick()等待异步操作完成:
it('should proceed to refresh token logic on 500 error', fakeAsync(inject([HttpClient, HttpTestingController], (http: HttpClient, httpMock: HttpTestingController) => { spyOn(service, "getNewTokenAndRetryRequest").and.stub(); const tokenStorage = TestBed.inject(TokenStorageService); spyOn(tokenStorage, 'getAccessToken').and.returnValue('mycookie'); http.get('/data').subscribe( () => {}, () => {} ); const req = httpMock.expectOne(r => r.headers.has('Authorization')); req.flush("failure",{ status: 500, statusText: "500 error" }); tick(); // 触发所有异步流程 expect(service.getNewTokenAndRetryRequest).toHaveBeenCalled(); httpMock.verify(); })));
4. 订阅回调覆盖不完整
你的subscribe只处理了成功回调,虽然拦截器在500错误时返回of([])会触发成功回调,但最好补充错误回调避免测试潜在报错。
内容的提问来源于stack exchange,提问作者Mr Smith

