最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

jasmine - How to unit-test an Angular v19 `resource` and trigger the evaluation of input-signal changes? - Stack Overflow

programmeradmin0浏览0评论

I have a Service which contains a resource (for a list of country-objects). Unit-testing the usual way seems not to work, even with fakeAsync and tick. Changes on the Subject which feeds the request-input do not trigger the loader.

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`);
  }
}

This is the unit-test:

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); // Simulates backend response
    tick();

    // assert [2]
    expect(service.countryListResource.value()).toEqual(mockCountries);
  }));
});

The test fails with (see assert [1]):

Error: Expected one matching request for criteria "Match URL: assets/countries/countries-en.json", found none.

In the output on the console, the subscription of the langChanges$ outputs, but the effect doesn't output anything.

Is there a way to trigger the evaluation of the resource request Signal?

BTW the service works without problems in production, only the unit-test causes me headaches.

I have a Service which contains a resource (for a list of country-objects). Unit-testing the usual way seems not to work, even with fakeAsync and tick. Changes on the Subject which feeds the request-input do not trigger the loader.

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`);
  }
}

This is the unit-test:

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); // Simulates backend response
    tick();

    // assert [2]
    expect(service.countryListResource.value()).toEqual(mockCountries);
  }));
});

The test fails with (see assert [1]):

Error: Expected one matching request for criteria "Match URL: assets/countries/countries-en.json", found none.

In the output on the console, the subscription of the langChanges$ outputs, but the effect doesn't output anything.

Is there a way to trigger the evaluation of the resource request Signal?

BTW the service works without problems in production, only the unit-test causes me headaches.

Share Improve this question asked Feb 3 at 10:19 hgoeblhgoebl 13k9 gold badges50 silver badges74 bronze badges 1
  • OTOMH, you need to await a promise. You could do that with a await appRef.whenStable(). – Matthieu Riegler Commented Feb 3 at 10:22
Add a comment  | 

1 Answer 1

Reset to default 1

My impression/explanation is that there is no mechanism to trigger the re-evaluation of the signals when the service is not embedded an a kind of application.

I tried to add ApplicationRef and call await appRef.whenStable(), but this didn't help (thanks for your comment @matthieu-riegler).

My first working solution was to add a pseudo component and use the fixture.detectChanges() of the component to trigger the signal re-evaluation. Read until the end!

@Component({ template: "<div></div>" })
class TestComponent {}

describe("CountryService", () => {
  // ...
  let fixture: ComponentFixture<TestComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [TestComponent],
      providers: [...]
    });

    // only used to trigger "detectChanges"
    fixture = TestBed.createComponent(TestComponent);
    // ...
  });
};

The test then looks like following. Keep an eye on fixture.detectChanges()!

  it("should retrieve a list of countries", fakeAsync(() => {
    // act
    languageSubject.next("en");
    fixture.detectChanges(); // <------
    tick();

    // ...

    // assert
    const req = httpMock.expectOne("assets/countries/countries-en.json");
    expect(req.request.method).toBe("GET");

    // act
    req.flush(mockCountries); // Simulates backend response
    fixture.detectChanges(); // <------
    tick();

    // assert (final state)
    expect(service.countryListResource.value()).toEqual(mockCountries);
  }));

The final (much shorter) solution:

But then I tried to just call TestBed.flushEffects() instead of fixture.detectChanges(). Seems like there is no need for a dummy component then.

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论