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 |1 Answer
Reset to default 1My 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.
await appRef.whenStable()
. – Matthieu Riegler Commented Feb 3 at 10:22