如何使用Jasmine对Angular中的HTTP拦截器进行单元测试
Angular HttpInterceptor 单元测试解决方案
现有代码问题梳理
- 拦截器注入token拼写错误:官方拦截器注入标识为
HTTP_INTERCEPTORS,你当前代码写为HTTP_INTERCEPTOR少了末尾的S,会导致拦截器未被正确注册,不生效 - 未触发请求结束逻辑:
finalize操作符仅在Observable流完成、报错时才会执行,你现有测试仅发起请求,没有调用flush/error方法触发请求结束,所以finalize内逻辑永远不会执行 - 拦截器逻辑存在笔误:拦截器开头判断url是
req.url !== '/getUsers'(带斜杠前缀),finalize内判断是req.url !== 'getUsers'(无斜杠),逻辑不一致会导致counter计数错误 - 未提供依赖的
LoaderServicemock实现,会导致依赖注入失败
第一步:修正拦截器笔误
// HttpInterceptorService.ts 修正finalize内的url判断逻辑 export class HttpInterceptorService implements HttpInterceptor { counter = 0; constructor(private loaderService: LoaderService) { } intercept(req: HttpRequest<any>, next: HttpHandler) { if (req.url !== '/getUsers') { this.counter ++; } this.loaderService.setStatus(true); return next.handle(req).pipe( finalize(() => { // 修正为和开头一致的判断条件 if (req.url !== '/getUsers') { this.counter --; } if (this.counter === 0) { this.loaderService.setStatus(false); } }) ); } }
第二步:编写完整单元测试代码
// HttpInterceptor.service.spec.ts import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { HTTP_INTERCEPTORS, HttpClient } from '@angular/common/http'; import { HttpInterceptorService } from './http-interceptor.service'; import { LoaderService } from './loader.service'; // mock LoaderService class MockLoaderService { setStatus = jasmine.createSpy('setStatus'); } describe('HttpInterceptorService', () => { let httpClient: HttpClient; let httpMock: HttpTestingController; let interceptor: HttpInterceptorService; let loaderService: LoaderService; beforeEach(()=> { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ HttpClient, {provide: HTTP_INTERCEPTORS, useClass: HttpInterceptorService, multi: true}, {provide: LoaderService, useClass: MockLoaderService} ] }); // Angular 9+ 推荐使用TestBed.inject代替TestBed.get httpClient = TestBed.inject(HttpClient); httpMock = TestBed.inject(HttpTestingController); interceptor = TestBed.inject(HTTP_INTERCEPTORS).find(i => i instanceof HttpInterceptorService) as HttpInterceptorService; loaderService = TestBed.inject(LoaderService); }); afterEach(() => { // 验证所有请求都被处理,没有遗留请求 httpMock.verify(); }); it('非/getUsers请求应该正确增减counter,且触发loader状态切换', ()=> { // 发起请求 httpClient.get('/getAdminList').subscribe(res => { expect(res).toBeTruthy(); }); // 先判断请求发起后counter加1,loader设为true expect(interceptor.counter).toBe(1); expect(loaderService.setStatus).toHaveBeenCalledWith(true); // flush触发请求结束,执行finalize逻辑 const testReq = httpMock.expectOne('/getAdminList'); testReq.flush({data: 'test'}); // 请求结束后counter减到0,loader设为false expect(interceptor.counter).toBe(0); expect(loaderService.setStatus).toHaveBeenCalledWith(false); }); it('/getUsers请求不修改counter值', () => { httpClient.get('/getUsers').subscribe(res => { expect(res).toBeTruthy(); }); expect(interceptor.counter).toBe(0); expect(loaderService.setStatus).toHaveBeenCalledWith(true); const testReq = httpMock.expectOne('/getUsers'); testReq.flush({data: 'user list'}); expect(interceptor.counter).toBe(0); expect(loaderService.setStatus).toHaveBeenCalledWith(false); }); it('多并行请求全部结束后才关闭loader', () => { // 发起3个非getUsers请求 httpClient.get('/api1').subscribe(); httpClient.get('/api2').subscribe(); httpClient.get('/api3').subscribe(); expect(interceptor.counter).toBe(3); expect(loaderService.setStatus).toHaveBeenCalledTimes(3); expect(loaderService.setStatus).toHaveBeenCalledWith(true); // 逐个结束请求 httpMock.expectOne('/api1').flush({}); expect(interceptor.counter).toBe(2); httpMock.expectOne('/api2').flush({}); expect(interceptor.counter).toBe(1); httpMock.expectOne('/api3').flush({}); expect(interceptor.counter).toBe(0); expect(loaderService.setStatus).toHaveBeenCalledWith(false); }); it('请求报错时也会执行finalize逻辑,正确重置counter和loader', () => { httpClient.get('/errorApi').subscribe({ error: (err) => expect(err).toBeTruthy() }); expect(interceptor.counter).toBe(1); const testReq = httpMock.expectOne('/errorApi'); // 触发请求报错 testReq.error(new ErrorEvent('network error'), {status: 500}); expect(interceptor.counter).toBe(0); expect(loaderService.setStatus).toHaveBeenCalledWith(false); }); });
核心说明
finalize执行的触发条件是Http请求流的结束,不管请求成功还是失败,只要你调用了flush或者error方法模拟请求结束,就会自动执行finalize内的逻辑,即可覆盖对应代码分支。
内容的提问来源于stack exchange,提问作者knbibin
相关产品推荐
相关产品推荐

