You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的惰性求值特性:

  1. language Signal由toSignal创建,只有当它被主动读取或在effect中使用时,才会触发计算。
  2. resource依赖language Signal,但测试中没有激活countryListResource的计算逻辑,导致loader从未被调用。
  3. 模拟的langChanges$ Subject没有初始值,language Signal初始为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 14:47:09