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

Angular Http拦截器spec.ts单元测试正确编写方法

Angular AuthInterceptor 100%覆盖率测试实现方案

现有测试的核心问题

之前的测试跑不通、断言不触发,本质是3个认知错误:

  • 断言对象错误:Angular的HttpRequest是不可变对象,拦截器里调用request.clone()会生成全新的请求实例传给下游,直接断言原始mockReq的headers永远拿不到修改后的值。
  • Observable逻辑错误:自定义的next.handle返回流里只调用了subscriber.complete(),没有调用subscriber.next()发送事件,subscribe的next回调根本不会触发,写在里面的断言自然不会执行。
  • 依赖注入不完整:TestBed没有显式提供ErrorService、CookieService的mock实现,容易触发真实服务的隐式逻辑,干扰覆盖率统计。

标准测试实现

以下写法不需要waitForAsync/fakeAsync,所有逻辑可被Karma正确识别,可实现100%代码覆盖率:

import { AuthInterceptor } from './auth.interceptor';
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { CookieService } from 'ngx-cookie-service';
import { ErrorService } from './error.service'; // 替换为项目实际路径
import { HttpRequest, HttpHandler, HttpResponse } from '@angular/common/http';
import { of } from 'rxjs';
import { Constants } from './constants'; // 替换为项目实际路径

describe('AuthInterceptor', () => {
  let interceptor: AuthInterceptor;
  let cookieServiceSpy: jasmine.SpyObj<CookieService>;
  let errorServiceSpy: jasmine.SpyObj<ErrorService>;
  let httpHandlerSpy: jasmine.SpyObj<HttpHandler>;

  beforeEach(() => {
    // 为所有依赖创建纯mock的spy对象,隔离真实服务逻辑
    cookieServiceSpy = jasmine.createSpyObj('CookieService', ['get']);
    errorServiceSpy = jasmine.createSpyObj('ErrorService', []);
    httpHandlerSpy = jasmine.createSpyObj('HttpHandler', ['handle']);

    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [
        AuthInterceptor,
        { provide: CookieService, useValue: cookieServiceSpy },
        { provide: ErrorService, useValue: errorServiceSpy }
      ]
    });

    interceptor = TestBed.inject(AuthInterceptor);
  });

  it('should create interceptor instance successfully', () => {
    expect(interceptor).toBeTruthy();
  });

  it('should attach csrf token and session id from cookies to cloned request headers', (done) => {
    // 准备测试数据
    const mockCsrfToken = 'mock-csrf-123';
    const mockSessionId = 'mock-session-456';
    const originRequest = new HttpRequest('GET', '/api/test');
    const mockSuccessResponse = new HttpResponse({ status: 200 });

    // 配置spy返回规则
    cookieServiceSpy.get.withArgs(Constants.COOKIES_KEYS.DTM_CSRF_TOKEN).and.returnValue(mockCsrfToken);
    cookieServiceSpy.get.withArgs(Constants.COOKIES_KEYS.DTM_SESSION_ID).and.returnValue(mockSessionId);
    httpHandlerSpy.handle.and.returnValue(of(mockSuccessResponse));

    // 执行拦截逻辑
    interceptor.intercept(originRequest, httpHandlerSpy).subscribe({
      next: (response) => {
        // 验证响应正常透传
        expect(response).toBe(mockSuccessResponse);
        // 捕获next.handle实际接收到的请求实例
        const handledRequest = httpHandlerSpy.handle.calls.mostRecent().args[0] as HttpRequest<unknown>;
        // 验证请求头已正确添加
        expect(handledRequest.headers.get('X-Csrf-Token')).toBe(mockCsrfToken);
        expect(handledRequest.headers.get('SessionId')).toBe(mockSessionId);
        // 验证原始请求未被修改(符合不可变设计要求)
        expect(originRequest.headers.has('X-Csrf-Token')).toBeFalse();
        expect(originRequest.headers.has('SessionId')).toBeFalse();
        done();
      }
    });
  });
});

关键说明

  • 用jasmine的done回调手动标记异步测试结束,完全规避waitForAsync无法识别内部断言的问题。由于of()返回的是同步Observable,上述逻辑实际执行时没有异步等待开销。
  • 所有依赖都用spy对象mock,既保证构造函数逻辑被完全覆盖,也不会触发真实服务的副作用。代码中注入但暂未使用的ErrorService只要被正确mock提供,就不会出现覆盖率遗漏。
  • 核心断言直接读取httpHandlerSpy.handle的调用参数,精准验证拦截器输出的请求对象是否符合预期,不会因为HttpRequest不可变特性出现断言失效。
  • 移除了不必要的RouterTestingModule引入,这个拦截器逻辑本身不依赖路由,不需要导入相关模块。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.01 23:45:35