如何在含await的Angular代码中使用HttpClientTestingModule测试?
问题
在给遗留Angular代码添加单元测试时发现,当代码使用await语法后,必须在每个httpTestingController.expect调用之间执行tick或flushMicrotask才能让测试通过。现有组件与测试代码如下:
组件代码
import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Component, OnInit } from '@angular/core'; import { firstValueFrom } from 'rxjs'; @Component({ selector: 'app-t-comp', standalone: true, imports: [], templateUrl: './t-comp.component.html', styleUrl: './t-comp.component.scss' }) export class TCompComponent implements OnInit { constructor(private http: HttpClient) {} ngOnInit(): void { // 该方法对应的测试可通过 this.init(); // 该方法对应的测试会失败,需处理异步时序 // this.initAwaits(); } init(): void { this.http.post('http://www.google.com', new HttpHeaders()).subscribe(googleResponse => { console.log(googleResponse); this.http.post('http://www.facebook.com', new HttpHeaders()).subscribe(facebookResponse => { console.log(facebookResponse); }); }); } async initAwaits(): Promise<void> { const googleResponse = await firstValueFrom(this.http.post('http://www.google.com', new HttpHeaders())); console.log(googleResponse); const facebookResponse = await firstValueFrom(this.http.post('http://www.facebook.com', new HttpHeaders())); console.log(facebookResponse); } }
测试代码
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { TCompComponent } from './t-comp.component'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; describe('TCompComponent', () => { let component: TCompComponent; let fixture: ComponentFixture<TCompComponent>; let httpTestingController: HttpTestingController; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [TCompComponent, HttpClientTestingModule] }) .compileComponents(); fixture = TestBed.createComponent(TCompComponent); component = fixture.componentInstance; fixture.detectChanges(); httpTestingController = TestBed.inject(HttpTestingController); }); afterEach(() => { // 每个测试后断言无未处理请求 httpTestingController.verify(); }); it('should create', () => { expect(component).toBeTruthy(); httpTestingController.expectOne({ method: 'POST', url: 'http://www.google.com', }, 'Google').flush({}); // 若调用initAwaits,需将测试改为fakeAsync并调用tick/flush httpTestingController.expectOne({ method: 'POST', url: 'http://www.facebook.com', }, 'Facebook').flush({}); }); });
希望能规避在每个expect之间处理时序的操作,优先定义所有httpTestingController的断言,最后通过一次tick或flush让组件完成所有逻辑执行。
解决方案
以下两种方式可满足需求:
1. 使用fakeAsync + flush一次性处理异步流程
将测试逻辑包裹在fakeAsync中,先定义所有请求预期,再通过flush()一次性触发所有异步任务推进流程,无需在每个请求间单独处理时序。修改后的测试代码如下:
it('should create', fakeAsync(() => { expect(component).toBeTruthy(); // 预先定义所有请求预期 const googleReq = httpTestingController.expectOne({ method: 'POST', url: 'http://www.google.com', }, 'Google'); const facebookReq = httpTestingController.expectOne({ method: 'POST', url: 'http://www.facebook.com', }, 'Facebook'); // 响应第一个请求 googleReq.flush({}); // 触发所有待处理微任务,推进到第二个请求执行阶段 flush(); // 响应第二个请求 facebookReq.flush({}); httpTestingController.verify(); }));
flush()会清空所有待处理的微任务与宏任务,一次性将异步流程推进到完成状态,避免了多次调用tick的繁琐。
2. 用match预先匹配所有请求(推荐)
使用httpTestingController.match()批量匹配所有符合条件的请求,再统一处理响应,无需关心请求的执行顺序,仅需验证请求是否存在:
it('should create', fakeAsync(() => { expect(component).toBeTruthy(); // 匹配所有POST请求 const requests = httpTestingController.match(req => req.method === 'POST'); // 根据URL区分并响应请求 requests.forEach(req => { if (req.url === 'http://www.google.com') { req.flush({}); } else if (req.url === 'http://www.facebook.com') { req.flush({}); } }); flush(); httpTestingController.verify(); }));
这种方式灵活性更高,即使请求顺序发生变化,测试也能正常通过,适合复杂异步场景。
核心原理
await会将后续逻辑封装为微任务,在第一个请求flush后,必须触发微任务队列才能让组件执行第二个请求的逻辑。fakeAsync配合flush或tick()可一次性清空微任务队列,避免了在每个断言间单独处理时序的问题。
内容的提问来源于stack exchange,提问作者sdparker
相关产品推荐
相关产品推荐

