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

如何使用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计数错误
  • 未提供依赖的LoaderService mock实现,会导致依赖注入失败

第一步:修正拦截器笔误

// 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.04 23:06:03