如何测试Angular 19的rxResource?调用未触发问题排查
Angular 19 rxResource 单元测试:请求未触发的问题及解决方案
问题描述
我正在对使用 Angular 19 新引入的 rxResource 工具的服务进行单元测试,目标服务代码如下:
import { computed, inject, Injectable, signal } from '@angular/core'; import { rxResource } from '@angular/core/rxjs-interop'; import { Observable, of } from 'rxjs'; import { RESULTS_REPOSITORY } from '../some-tokens'; @Injectable() export class CommonResultsDataStore { readonly query = signal<string | null>(null); private readonly repositoryService = inject<{ search: (v: string) => Observable<object[]>; }>(RESULTS_REPOSITORY); private readonly resource = rxResource({ request: () => this.query(), loader: ({ request }) => request ? this.repositoryService.search(request) : of(null), }); data = computed(() => this.resource.value() ?? []); }
编写的 Jest 测试套件如下:
import { TestBed } from '@angular/core/testing'; import { Subject } from 'rxjs'; import { RESULTS_REPOSITORY } from '../some-tokens'; import { CommonResultsDataStore } from '.'; describe('CommonResultsDataStore', () => { const searchSubject = new Subject<object[]>(); const mockRepository = { search: jest.fn((_v: string) => searchSubject.asObservable()), }; let service: CommonResultsDataStore; beforeEach(() => { TestBed.configureTestingModule({ providers: [ { provide: RESULTS_REPOSITORY, useValue: mockRepository }, CommonResultsDataStore, ], }); service = TestBed.inject(CommonResultsDataStore); }); describe('initial state', () => { it('should return empty results', () => { expect(service.data()).toStrictEqual([]); expect(mockRepository.search).not.toHaveBeenCalled(); }); }); describe('with remote query', () => { beforeEach(() => { service.query.set('toto'); }); it('should return empty results', () => { expect(service.data()).toStrictEqual([]); expect(mockRepository.search).toHaveBeenCalledTimes(1); // ❌ 未触发调用 expect(mockRepository.search).toHaveBeenCalledWith('toto'); }); it('should return results', () => { // When const flushedData = [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }, ]; searchSubject.next(flushedData); // Then expect(service.data()).toStrictEqual([]); expect(mockRepository.search).toHaveBeenCalledTimes(1); // ❌ 未触发调用 expect(mockRepository.search).toHaveBeenCalledWith('toto'); }); }); });
问题:设置 query 信号后,预期会调用 repository 服务,但实际未触发任何调用。尝试过使用 toObservable 工具及在设置信号前手动订阅,均未成功。
问题原因
- rxResource 的异步调度特性:
rxResource内部使用 RxJS 的异步调度器(默认是asyncScheduler)处理请求触发逻辑。当request信号(即query)变化时,loader 函数的执行是异步的,测试中同步执行断言时,异步操作尚未完成。 - 断言时机过早:当前测试未使用 Angular 提供的异步测试工具控制流程,导致在 loader 函数还未被调用时就执行了断言。
推荐测试方式
使用 Angular 测试工具 fakeAsync 和 tick() 模拟异步流程,确保在断言前等待所有异步操作完成。同时调整测试结构,避免测试间的状态干扰。
修改后的测试代码
import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { Subject } from 'rxjs'; import { RESULTS_REPOSITORY } from '../some-tokens'; import { CommonResultsDataStore } from '.'; describe('CommonResultsDataStore', () => { const searchSubject = new Subject<object[]>(); const mockRepository = { search: jest.fn((_v: string) => searchSubject.asObservable()), }; let service: CommonResultsDataStore; beforeEach(() => { // 重置 mock 和 subject 状态,避免测试间污染 jest.clearAllMocks(); searchSubject.next([]); TestBed.configureTestingModule({ providers: [ { provide: RESULTS_REPOSITORY, useValue: mockRepository }, CommonResultsDataStore, ], }); service = TestBed.inject(CommonResultsDataStore); }); describe('initial state', () => { it('should return empty results', () => { expect(service.data()).toStrictEqual([]); expect(mockRepository.search).not.toHaveBeenCalled(); }); }); describe('with remote query', () => { it('should trigger search and return empty results initially', fakeAsync(() => { // When service.query.set('toto'); tick(); // 推进时间,让 rxResource 完成异步请求调度 // Then expect(mockRepository.search).toHaveBeenCalledTimes(1); expect(mockRepository.search).toHaveBeenCalledWith('toto'); expect(service.data()).toStrictEqual([]); })); it('should return results when search emits data', fakeAsync(() => { // Given service.query.set('toto'); tick(); // 等待搜索请求触发 // When const flushedData = [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }, ]; searchSubject.next(flushedData); tick(); // 等待 rxResource 处理返回的数据 // Then expect(service.data()).toStrictEqual(flushedData); expect(mockRepository.search).toHaveBeenCalledTimes(1); })); }); });
关键修改说明
- 使用
fakeAsync包裹测试函数:创建虚拟异步测试环境,允许手动控制时间推进。 - 调用
tick():模拟时间流逝,触发所有待处理的异步操作(包括 rxResource 的 loader 执行、信号更新)。 - 重置测试状态:在
beforeEach中清除 mock 调用记录并重置 subject,确保每个测试的初始状态独立。 - 调整测试结构:将
query.set移至测试函数内部,替代beforeEach,避免测试间的状态干扰。
内容的提问来源于stack exchange,提问作者fservantdev
相关产品推荐
相关产品推荐

