Angular v19 resource单元测试:如何触发输入信号变更执行loader?
Angular单元测试中resource未触发loader的问题解决
问题背景
我有一个基于Angular Signals的CountryService,使用resource根据语言切换获取国家列表。生产环境运行正常,但单元测试时,即便用了fakeAsync和tick,当作为请求源的Subject更新时,resource的loader也不会触发,导致HTTP请求未发出,测试失败。
服务代码
export class CountryService { private http = inject(HttpClient); private translocoService = inject(TranslocoService); private language = toSignal(this.translocoService.langChanges$); public readonly countryListResource = resource({ request: this.language, loader: (params) => { const language = params.request; return lastValueFrom(this.fetchCountries(language)); }, }).asReadonly(); public constructor() { effect(() => console.log("CountryService: language changed to", this.language())); this.translocoService.langChanges$.subscribe((lang) => console.log("translocoService.langChanges$ changed: " + lang), ); } private fetchCountries(language: string): Observable<Country[]> { return this.http .get<Country[]>(`assets/countries/countries-${language}.json`); } }
测试代码
describe("CountryService", () => { let service: CountryService; let httpMock: HttpTestingController; let languageSubject = new Subject<string>(); beforeEach(() => { TestBed.configureTestingModule({ providers: [ provideHttpClient(), provideHttpClientTesting(), { provide: TranslocoService, useValue: { langChanges$: languageSubject } }, ], }); service = TestBed.inject(CountryService); httpMock = TestBed.inject(HttpTestingController); }); it("should retrieve a list of countries from assets", fakeAsync(() => { const expectedEmpty = service.countryListResource.value(); expect(expectedEmpty).toBeUndefined(); // act languageSubject.next("en"); tick(); // assert [1] - 此处失败 const req = httpMock.expectOne("assets/countries/countries-en.json"); // act req.flush(mockCountries); // 模拟后端响应 tick(); // assert [2] expect(service.countryListResource.value()).toEqual(mockCountries); })); });
错误信息
Error: Expected one matching request for criteria "Match URL: assets/countries/countries-en.json", found none.
控制台仅输出langChanges$的订阅日志,effect无任何输出。
问题原因
核心在于Angular Signals的惰性求值特性:
languageSignal由toSignal创建,只有当它被主动读取或在effect中使用时,才会触发计算。resource依赖languageSignal,但测试中没有激活countryListResource的计算逻辑,导致loader从未被调用。- 模拟的
langChanges$Subject没有初始值,languageSignal初始为undefined,后续的语言切换无法触发resource的请求更新(因为没有主动触发Signal的变化检测)。
解决方案
方案1:主动读取Signal值+触发Effects
通过手动读取countryListResource.value()激活计算,并用flushEffects()触发所有待处理的Signal相关逻辑:
describe("CountryService", () => { let service: CountryService; let httpMock: HttpTestingController; let languageSubject = new Subject<string>(); beforeEach(() => { TestBed.configureTestingModule({ providers: [ provideHttpClient(), provideHttpClientTesting(), { provide: TranslocoService, useValue: { langChanges$: languageSubject } }, ], }); service = TestBed.inject(CountryService); httpMock = TestBed.inject(HttpTestingController); // 给Subject设置初始值,确保Signal有初始状态 languageSubject.next("de"); flushEffects(); // 触发初始Signal计算 }); it("should retrieve a list of countries from assets", fakeAsync(() => { // 主动读取value激活resource的初始计算 expect(service.countryListResource.value()).toBeUndefined(); // act languageSubject.next("en"); flushEffects(); // 触发Signal变化后的loader调用 // assert [1] const req = httpMock.expectOne("assets/countries/countries-en.json"); // act req.flush(mockCountries); tick(); // assert [2] expect(service.countryListResource.value()).toEqual(mockCountries); })); });
方案2:创建临时Effect激活Resource
通过创建一个临时effect订阅countryListResource,确保它始终处于活跃状态:
it("should retrieve a list of countries from assets", fakeAsync(() => { // 创建临时Effect,强制激活resource的计算逻辑 const testEffect = effect(() => service.countryListResource.value()); expect(service.countryListResource.value()).toBeUndefined(); // act languageSubject.next("en"); flushEffects(); // assert [1] const req = httpMock.expectOne("assets/countries/countries-en.json"); // act req.flush(mockCountries); tick(); // assert [2] expect(service.countryListResource.value()).toEqual(mockCountries); // 清理临时Effect testEffect.destroy(); }));
核心总结
Angular Signals是惰性求值的,测试环境中没有生产环境的自动触发机制,必须通过以下方式激活计算:
- 主动读取Signal的
value() - 创建
effect订阅目标Signal - 使用
flushEffects()触发所有待处理的Signal相关逻辑
内容的提问来源于stack exchange,提问作者hgoebl
相关产品推荐
相关产品推荐

