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
相关产品推荐
相关产品推荐

