Angular单元测试:如何测试结合AsyncPipe使用的RxJS timer
Angular单元测试:如何测试结合AsyncPipe使用的RxJS timer
我在为一个结合AsyncPipe在模板中展示的RxJS timer编写单元测试时碰了壁,以下是我的组件代码和对应的测试代码:
组件代码
import { Component } from '@angular/core'; import { AsyncPipe, NgIf } from '@angular/common'; import { map, timer } from 'rxjs'; @Component({ selector: 'app-root', template: ` Timer should fire after {{ delay }} seconds: <ng-container *ngIf="timer$ | async as timer; else waiting">Timer fired {{ timer }} times</ng-container> <ng-template #waiting>Waiting for Timer…</ng-template> `, imports: [AsyncPipe, NgIf], }) export class AppComponent { delay = 3; timer$ = timer(this.delay * 1000, 1000).pipe(map((n) => n + 1)); }
测试代码
import { AppComponent } from './main'; import { ComponentFixture, TestBed, } from '@angular/core/testing'; import { firstValueFrom } from 'rxjs'; describe('AppComponent', () => { let component: AppComponent; let fixture: ComponentFixture<AppComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [AppComponent], }).compileComponents(); fixture = TestBed.createComponent(AppComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should initially render waiting', () => { expect(fixture.nativeElement.textContent).toContain('Waiting'); //passes }); it('should emit timer', async () => { expect(await firstValueFrom(component.timer$)).toBe(1); //passes }); it('should show "Timer fired"', () => { jest.useFakeTimers(); jest.advanceTimersByTime(5000); expect(fixture.nativeElement.textContent).toContain('Timer fired'); //fails }); });
问题分析
当前失败的测试用例里,虽然用jest.advanceTimersByTime快进了时间,但有两个关键问题:
- RxJS的
timer默认不会自动感知jest的fake timers,需要配置RxJS适配jest的计时器模拟; - 时间快进后,没有触发Angular的变更检测,模板不会更新。
解决方案
我们可以结合Angular的测试工具,或者配置RxJS适配jest的fake timers,同时触发变更检测来更新模板。下面是两种可行的修正方案:
方案一:使用Angular的fakeAsync和tick
这是Angular测试中处理异步计时器最推荐的方式,它会自动适配RxJS的调度器:
import { AppComponent } from './main'; import { ComponentFixture, TestBed, fakeAsync, tick, } from '@angular/core/testing'; import { firstValueFrom } from 'rxjs'; describe('AppComponent', () => { let component: AppComponent; let fixture: ComponentFixture<AppComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [AppComponent], }).compileComponents(); fixture = TestBed.createComponent(AppComponent); component = fixture.componentInstance; fixture.detectChanges(); }); // 保留原有通过的测试用例... it('should show "Timer fired"', fakeAsync(() => { // 快进到timer第一次触发的时间点 tick(component.delay * 1000); // 手动触发变更检测,让模板更新 fixture.detectChanges(); expect(fixture.nativeElement.textContent).toContain('Timer fired'); // 还可以拓展测试后续的timer触发 tick(1000); fixture.detectChanges(); expect(fixture.nativeElement.textContent).toContain('Timer fired 2 times'); })); });
方案二:配置RxJS适配jest的fake timers
如果更习惯使用jest的fake timers,可以手动配置RxJS使用全局计时器,同时记得触发变更检测:
import { AppComponent } from './main'; import { ComponentFixture, TestBed, } from '@angular/core/testing'; import { firstValueFrom, rxjs } from 'rxjs'; // 配置RxJS使用全局的jest模拟计时器 rxjs.scheduler.setTimeout.setWindow(window); describe('AppComponent', () => { let component: AppComponent; let fixture: ComponentFixture<AppComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [AppComponent], }).compileComponents(); fixture = TestBed.createComponent(AppComponent); component = fixture.componentInstance; fixture.detectChanges(); }); // 保留原有通过的测试用例... it('should show "Timer fired"', () => { jest.useFakeTimers(); jest.advanceTimersByTime(5000); // 触发变更检测更新视图 fixture.detectChanges(); expect(fixture.nativeElement.textContent).toContain('Timer fired'); // 用完记得恢复真实计时器 jest.useRealTimers(); }); });
关键总结
原来的测试失败核心原因有两点:
- Angular不会自动在jest快进时间后更新视图,必须手动调用
fixture.detectChanges()触发变更检测; - RxJS的
timer默认使用自身调度器,不会识别jest的fake timers,要么用Angular的fakeAsync自动适配,要么手动配置RxJS使用全局计时器。
备注:内容来源于stack exchange,提问作者reins.ch
相关产品推荐
相关产品推荐

