news 2026/10/5 1:56:57

Angular 测试 HTTP 请求实战:使用 @angular/common/http/testing 模拟后端交互

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Angular 测试 HTTP 请求实战:使用 @angular/common/http/testing 模拟后端交互
  • 文档
  • 教程
  • 知识库

【免费下载链接】developer-roadmap

Interactive roadmaps, guides and other educational content to help developers grow in their careers.

项目地址:https://gitcode.com/GitHub_Trending/de/developer-roadmap
点击查看免费下载

导读

在 Angular 应用中,几乎所有业务逻辑都绕不开HttpClient发起的网络请求;而测试一旦真实触网,就会变得缓慢、不稳定且受外部环境制约。本文基于 developer-roadmap 仓库中 roadmaps/angular/content/testing-requests 的知识点展开,系统讲解如何使用 Angular 官方提供的@angular/common/http/testing库,在单元测试中拦截(mock)应用发出的 HTTP 请求、对请求本身做断言,并模拟后端返回响应,从而写出不依赖真实服务器、快速且确定性的测试。读完本文,你将掌握HttpTestingController的完整用法、常见场景的测试代码模板,以及请求测试与 Testing Services、Making Requests 等知识点的衔接关系。

为什么必须 Mock HTTP 后端

正如测试请求文档开篇所指出的:"As for any external dependency, you must mock the HTTP backend so your tests can simulate interaction with a remote server."(与任何外部依赖一样,你必须模拟 HTTP 后端,让测试能够模拟与远程服务器的交互)。

在 Angular 中,HTTP Client 是应用与服务器通信的标准通道,HttpClient服务类来自@angular/common/http。它暴露了对应不同 HTTP 动词的方法(get、post、put、delete、patch等),每个方法返回一个 RxJSObservable——只有订阅(subscribe)时请求才会真正发出,并在服务器响应后把结果推送给订阅者(参见 Making Requests 与 Observable Pattern)。

在测试中保留真实 HTTP 调用会带来一系列问题:

  • 不确定性:网络延迟、服务端状态、限流(rate limiting)都会让测试结果飘忽不定;
  • 缓慢:每次运行测试都等待真实网络往返,拖慢 CI 与本地开发反馈;
  • 副作用:POST/PUT/DELETE 等变更型请求可能污染真实数据;
  • 难以复现:难以构造错误码、超时、空响应等边界场景。

因此 Angular 提供了专门的测试工具库@angular/common/http/testing,其核心职责有三点,恰好对应文档描述的三大能力:

  1. 捕获(capture)应用发出的请求;
  2. 断言(assert)这些请求的 URL、方法、请求体、请求头等细节;
  3. 模拟响应(mock responses)以仿真后端行为,让被测试代码拿到预期数据。

这套机制让测试在完全脱离网络的条件下,依然能够完整验证"发什么请求、拿什么数据、怎么处理"的整条链路。

测试环境的搭建:引入 HttpClientTestingModule

要让测试拦截 HTTP 请求,第一步是在测试模块中注入测试专用的提供者。Angular 提供了两种等价方式:

方式一:NgModule 方式(传统写法)

在 TestBed 的imports中引入HttpClientTestingModule:

import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing'; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], }); });

HttpClientTestingModule会替换掉真实的HttpClient后端实现,使所有经由HttpClient发起的请求都被测试后端捕获,而不会真正触网。

方式二:provideHttpClientTesting() 函数式写法(现代写法)

从 Angular 15 开始,HttpClient推荐通过provideHttpClient(...)系列函数提供。与之配套的测试提供者就是provideHttpClientTesting():

import { TestBed } from '@angular/core/testing'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; beforeEach(() => { TestBed.configureTestingModule({ providers: [ provideHttpClient(), // 启用 HttpClient 及其功能 provideHttpClientTesting(), // 用测试后端替换真实后端 ], }); });

两者效果相同,选择哪一种取决于项目的整体风格——如果你的应用主体采用provideHttpClient函数式提供(常见于 standalone 应用),测试中应保持一致的写法;反之则使用HttpClientTestingModule。

注入 HttpTestingController

HttpTestingController是测试后端的"遥控器",负责查询、断言和刷新请求。它通过构造函数注入获取:

import { HttpTestingController } from '@angular/common/http/testing'; let httpTestingController: HttpTestingController; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], }); httpTestingController = TestBed.inject(HttpTestingController); }); afterEach(() => { // 关键步骤:验证没有遗漏的、未处理的请求 httpTestingController.verify(); });

afterEach中的verify()是容易被新手忽略但极其重要的收尾动作:它会断言测试期间没有未处理(既未 flush 也未 match 掉)的请求。若应用代码意外多发了请求而测试没有响应它,verify()会抛出错误,从而把"漏网请求"变成显式的测试失败。

核心用法:捕获请求、断言细节、模拟响应

HttpTestingController提供的核心 API 如下表所示:

方法用途
expectOne(url | matcher)断言恰好只有一个请求匹配给定的 URL 或匹配函数,并返回该请求的句柄TestRequest;若数量为 0 或多个则抛错
match(url | matcher)返回所有匹配的请求数组(不限制数量),常用于并发请求或多请求场景
TestRequest.flush(body, opts?)模拟服务器返回响应:给出响应体,可附加状态码与响应头
TestRequest.error(error, opts?)模拟网络错误或 HTTP 错误响应
controller.verify()断言所有请求都已处理,无遗留请求

典型流程示例:测试一个 DataService

假设有一个通过HttpClient拉取用户数据的服务:

// data.service.ts import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; export interface User { id: number; name: string; } @Injectable({ providedIn: 'root' }) export class DataService { constructor(private http: HttpClient) {} getUser(id: number): Observable<User> { return this.http.get<User>(`/api/users/${id}`); } }

对应的测试:

import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { DataService } from './data.service'; describe('DataService', () => { let service: DataService; let httpTestingController: HttpTestingController; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [DataService], }); service = TestBed.inject(DataService); httpTestingController = TestBed.inject(HttpTestingController); }); afterEach(() => { httpTestingController.verify(); }); it('should fetch a user and emit its data', () => { const mockUser: User = { id: 1, name: 'Ada' }; let actualUser: User | undefined; // 触发请求(订阅后 HttpClient 才会真正发出请求) service.getUser(1).subscribe(user => (actualUser = user)); // 断言恰好发出了一个指向目标 URL 的 GET 请求 const req = httpTestingController.expectOne('/api/users/1'); expect(req.request.method).toBe('GET'); // 模拟后端返回响应体 req.flush(mockUser); // 验证订阅者收到了数据 expect(actualUser).toEqual(mockUser); }); });

这段测试完整呈现了"捕获 → 断言 → 模拟响应"三步骤:expectOne捕获并校验请求,req.request.method断言 HTTP 动词,req.flush(mockUser)让后端"返回"数据,最终actualUser收到响应值。

断言请求细节:URL、方法、请求体、请求头

TestRequest暴露了底层请求的完整信息,可以对请求做细粒度断言:

it('should send POST with expected body and headers', () => { const payload = { name: 'New User' }; service.createUser(payload).subscribe(); const req = httpTestingController.expectOne('/api/users'); // 断言 HTTP 动词 expect(req.request.method).toBe('POST'); // 断言请求体 expect(req.request.body).toEqual(payload); // 断言请求头 expect(req.request.headers.get('Content-Type')).toBe('application/json'); // 返回 201 创建成功,并带上响应头 req.flush({ id: 2, name: 'New User' }, { status: 201, statusText: 'Created', headers: { 'X-Trace-Id': 'abc-123' }, }); });

flush的第二参数支持status(状态码)、statusText(状态文本)与headers(响应头),足以覆盖绝大多数后端行为的模拟。

进阶场景

场景一:断言没有多余请求

如果希望确认某个操作不会触发请求,可以用expectNone类语义——但HttpTestingController本身不提供expectNone,更常见的做法是结合match与verify:

it('should not call the API when guard fails', () => { service.loadIfAllowed(false).subscribe(); // match 返回匹配请求数组,此处期望为空 const requests = httpTestingController.match('/api/users'); expect(requests.length).toBe(0); });

场景二:处理并发/多次请求

当一次操作会发出多个请求(例如多个并行加载),expectOne会因为"匹配到多个"而抛错,此时应使用match:

it('should handle multiple parallel requests', () => { service.loadDashboard().subscribe(); const requests = httpTestingController.match('/api/stats'); // 逐个模拟响应 requests[0].flush({ visits: 100 }); requests[1].flush({ sales: 42 }); // 断言数量符合预期 expect(requests.length).toBe(2); });

场景三:模拟错误响应与网络错误

TestRequest还支持通过error()模拟失败路径,用于测试错误处理逻辑:

import { HttpErrorResponse } from '@angular/common/http'; it('should surface server errors', () => { const errorSpy = jasmine.createSpy('error'); service.getUser(1).subscribe({ next: () => fail('should not succeed'), error: errorSpy, }); const req = httpTestingController.expectOne('/api/users/1'); // 模拟 HTTP 400 错误 req.flush('Not Found', { status: 404, statusText: 'Not Found' }); // 或者模拟网络层错误(如断网): // req.error(new ProgressEvent('network error')); expect(errorSpy).toHaveBeenCalled(); expect(errorSpy.calls.mostRecent().args[0]).toBeInstanceOf(HttpErrorResponse); });

req.error(new ProgressEvent('network error'))会以HttpErrorResponse的形式产生一个 status 为 0 的网络错误,与真实断网场景一致。

场景四:结合 RxJS 延迟断言异步数据

由于HttpClient方法返回的是Observable,订阅回调是异步执行的,需要借助 Jasmine 的fakeAsync/tick或waitForAsync来正确推进测试:

import { fakeAsync, tick } from '@angular/core/testing'; it('should fetch user asynchronously', fakeAsync(() => { let actualUser: User | undefined; service.getUser(1).subscribe(user => (actualUser = user)); const req = httpTestingController.expectOne('/api/users/1'); req.flush({ id: 1, name: 'Ada' }); // 推进微任务队列,让订阅回调执行 tick(); expect(actualUser).toEqual({ id: 1, name: 'Ada' }); }));

实际上flush会同步触发响应流,多数简单场景无需tick;但当响应之后还有delay、switchMap等 RxJS 操作符时,fakeAsync+tick是控制异步推进的必备手段。

场景五:使用匹配函数做灵活断言

expectOne/match的第一个参数除了字符串 URL,还可以是匹配函数,便于对 URL 做正则或条件判断:

const req = httpTestingController.expectOne( (r) => r.url.startsWith('/api/users/') && r.method === 'GET' ); expect(req.request.url).toBe('/api/users/1');

这在 URL 带动态参数(如查询串、ID 片段)的场景下特别有用。

在组件测试中使用 HttpTestingController

HTTP 测试不只用于 Service 单测,也常用于组件测试:组件模板通过服务加载数据并渲染,测试中通过HttpTestingController控制数据供给,从而验证组件的渲染与交互。

import { ComponentFixture, TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { UserListComponent } from './user-list.component'; describe('UserListComponent', () => { let fixture: ComponentFixture<UserListComponent>; let httpTestingController: HttpTestingController; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [HttpClientTestingModule], declarations: [UserListComponent], }).compileComponents(); fixture = TestBed.createComponent(UserListComponent); httpTestingController = TestBed.inject(HttpTestingController); }); afterEach(() => httpTestingController.verify()); it('should render users from the mocked backend', () => { fixture.detectChanges(); // 触发 ngOnInit 中的请求 const req = httpTestingController.expectOne('/api/users'); req.flush([{ id: 1, name: 'Ada' }, { id: 2, name: 'Grace' }]); fixture.detectChanges(); // 触发变更检测,渲染列表 const items = fixture.nativeElement.querySelectorAll('.user-item'); expect(items.length).toBe(2); }); });

这里需要留意fixture.detectChanges()的调用时机:第一次调用触发组件初始化进而发出请求,flush返回数据后再次调用触发变更检测完成渲染——这正是"捕获请求、断言、模拟响应"三步骤在组件场景中的自然延伸。

常见错误与调试技巧

问题原因解决
expectOne抛 "Expected one matching request, found none"请求尚未发出或 URL 不匹配确认已调用subscribe/fixture.detectChanges();核对 URL 与请求方法
expectOne抛 "Expected one matching request, found multiple"匹配到了多个请求改用match并逐个处理
verify()抛 "Expected no open requests, found X"存在未 flush 的请求在afterEach调用verify,并确保每个请求都有对应的flush/error
订阅回调未执行缺少异步推进使用fakeAsync+tick或waitForAsync
401/403 被吞掉应用内存在认证拦截器测试中同样提供拦截器(HTTP_INTERCEPTORS)后再断言

与其他测试主题的衔接

在 Angular 学习路线图中,"Testing Requests" 是测试知识体系中的一环,它与仓库内其他主题紧密关联:

  • Testing Services:Service 是最容易做单元测试的类,在beforeEach中实例化、调用方法并断言结果;当 Service 依赖HttpClient时,就必须用本文的HttpTestingController来拦截请求;
  • Making Requests:理解HttpClient各动词方法返回Observable的语义,是写出正确请求断言的前提;
  • Writing Interceptors:认证、日志、重试等拦截器同样需要测试,HttpClientTestingModule会一并拦截经过拦截器链的请求;
  • HTTP Client 与 Observable Pattern:HttpClient的 Observable 流式语义,决定了测试必须控制订阅时机与异步推进。

总结

Angular 官方测试请求的核心结论可以浓缩为三点:

  1. 一律 Mock:HttpClient是外部依赖,测试必须通过@angular/common/http/testing模拟后端,杜绝真实网络调用;
  2. 三步走:HttpTestingController.expectOne/match捕获并断言请求 → 检查req.request的 method/body/headers →req.flush/req.error模拟响应;
  3. 收尾必查:afterEach中调用verify(),确保没有任何未处理的请求泄漏到测试之外。

掌握这套模式后,无论是 Service 单测、组件集成测试还是拦截器测试,你都能在毫秒级、零网络依赖的前提下,完整验证应用与后端之间的每一个交互细节。

  • 文档
  • 教程
  • 知识库

【免费下载链接】developer-roadmap

Interactive roadmaps, guides and other educational content to help developers grow in their careers.

项目地址:https://gitcode.com/GitHub_Trending/de/developer-roadmap
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!