如何测试包含内部订阅的Angular 18服务?
Angular 18 TimesheetService 单元测试问题
我有一个Angular 18的TimesheetService需要进行单元测试,其中setSelected方法调用后,会通过内部订阅apiService.getTimesheet$来更新BehaviorSubject。当前测试代码出现超时错误,我需要验证该方法调用后,外部可观察对象selectedTimesheet$和isLoading$在订阅完成前后的状态,但不知道如何等待内部订阅完成。
服务代码
@Injectable({ providedIn: 'root' }) export class TimesheetService { private readonly apiService = inject(ApiService); private readonly selectedTimesheetSubject$ = new BehaviorSubject<ITimesheet| undefined>(undefined); private readonly isLoadingSubject$ = new BehaviorSubject(false); public readonly selectedTimesheet$ = this.selectedTimesheetSubject$.asObservable(); public readonly isLoading$ = this.isLoadingSubject$.asObservable(); public setSelected(weekEndingDate: string): void { this.isLoadingSubject$.next(true); this.error = undefined; this.apiService.getTimesheet$(weekEndingDate).subscribe({ next: (res) => { this.isLoadingSubject$.next(false); this.selectedTimesheetSubject$.next(res); }, error: (err) => { this.isLoadingSubject$.next(false); console.error('error!', err); }, }); } public clearSelected(): void { this.selectedTimesheetSubject$.next(undefined); } }
测试代码
it('should set the selected timesheet based on a passed in date', async () => { const weekEnding = DateTime.fromISO('2024-11-02'); const expectedInterval = Interval.fromDateTimes(weekEnding, weekEnding); injectedApiService.getTimesheet$.and.nextOneTimeWith([mockSelfTimesheet]); service.setSelected('2024-11-02'); //Check that the initial values are correct BEFORE the subscription completes expect(await lastValueFrom(service.isLoading$)).toBeTrue(); expect(await lastValueFrom(service.selectedTimesheet$)).toBeUndefined(); expect(injectedApiService.getTimesheet$).toHaveBeenCalledOnceWith(expectedInterval, false); //How do I check these values AFTER the subscription completes? // expect(await lastValueFrom(service.isLoading$)).toBeFalse(); // expect(await lastValueFrom(service.selectedTimesheet$)).toEqual(mockSelfTimesheet); });
当前错误
Error: Timeout - Async function did not complete within 5000ms (set by jasmine.DEFAULT_TIMEOUT_INTERVAL)
解决方案
核心问题是测试中错误地用lastValueFrom订阅了永久流(BehaviorSubject的可观察对象不会主动完成),导致测试一直等待超时;同时未正确同步内部异步订阅的执行时机。
1. 推荐方案:使用fakeAsync + tick同步控制异步流
Angular的fakeAsync可以把异步操作转为同步执行,tick()会触发所有待处理的微任务,确保内部订阅的回调执行完成:
it('should set the selected timesheet based on a passed in date', fakeAsync(() => { const weekEnding = DateTime.fromISO('2024-11-02'); const expectedInterval = Interval.fromDateTimes(weekEnding, weekEnding); const mockTimesheet = mockSelfTimesheet; // 模拟API返回同步流 injectedApiService.getTimesheet$.and.returnValue(of(mockTimesheet)); // 调用目标方法 service.setSelected('2024-11-02'); // 捕获初始状态(订阅未完成时) let currentLoading: boolean; let currentSelected: ITimesheet | undefined; service.isLoading$.subscribe(val => currentLoading = val); service.selectedTimesheet$.subscribe(val => currentSelected = val); expect(currentLoading).toBeTrue(); expect(currentSelected).toBeUndefined(); expect(injectedApiService.getTimesheet$).toHaveBeenCalledOnceWith(expectedInterval, false); // 触发所有异步操作完成 tick(); // 验证订阅完成后的最终状态 expect(currentLoading).toBeFalse(); expect(currentSelected).toEqual(mockTimesheet); }));
2. 替代方案:使用firstValueFrom等待状态变化
如果不想用fakeAsync,可以通过过滤流的方式,等待状态变化的第一个有效值:
it('should set the selected timesheet based on a passed in date', async () => { const weekEnding = DateTime.fromISO('2024-11-02'); const expectedInterval = Interval.fromDateTimes(weekEnding, weekEnding); const mockTimesheet = mockSelfTimesheet; injectedApiService.getTimesheet$.and.returnValue(of(mockTimesheet)); // 提前订阅状态,避免错过初始值 const loading$ = service.isLoading$; const selected$ = service.selectedTimesheet$; // 调用目标方法 service.setSelected('2024-11-02'); // 验证初始状态 expect(await firstValueFrom(loading$)).toBeTrue(); expect(await firstValueFrom(selected$)).toBeUndefined(); expect(injectedApiService.getTimesheet$).toHaveBeenCalledOnceWith(expectedInterval, false); // 等待loading变为false的第一个值,确保订阅完成 await firstValueFrom(loading$.pipe(filter(val => val === false))); // 验证最终状态 expect(await firstValueFrom(selected$)).toEqual(mockTimesheet); });
关键注意事项
- 永远不要用
lastValueFrom订阅永久流(比如BehaviorSubject导出的可观察对象),它会一直等待流完成,直接导致超时。 - 使用
fakeAsync时,确保所有异步操作都是RxJS或Angular可跟踪的,第三方异步逻辑可能无法被捕获。 - 模拟API接口时,尽量返回同步流(比如
of()),配合tick()可以让内部订阅立即执行回调。
内容的提问来源于stack exchange,提问作者Chris Barr
相关产品推荐
相关产品推荐

