I have a private function that is called from my constructor and uses toObservable on a computed Signal. It works great in practice, but trying to write unit tests has been a PITA. I'm sure it's just something simple on my part, but I can't get all the injection contexts to line up, or get jasmine to create a proper spy.
AuthService:
// check token state action
private expiredStatus$ = toObservable(this.permissionStore.expiredStatus)
constructor() {
console.log("Constructor started....");
this.expiredStatus$
.pipe(
tap(() => {
return console.log("Subscription Active");
})
)
.subscribe({
next: (tokenStatus) => {
console.log("Token refreshed successfully, new status:", tokenStatus);
},
error: (error) => {
console.error("Failed to refresh token:", error);
},
});
}
PermissionStoreService:
// true if we have an Expired state on the token
expiredStatus: Signal<boolean> = computed(() => this.permissionsState().token_status === TokenStatus.Expired);
I am trying to test expiredStatus$ by mocking this.permissionStore.expiredStatus, and turn it from false to true, after the AuthService has been initialized.
Since expiredStatus$ is subscribed to in the AuthService constructor, I am running into issues where my mocks are in a different injection context or something weird. jasmine.createSpy / spyOn / spyObj also don't recognize the
If I force an acutal computed signal value, I can get the constructor to run.
TestBed.configureTestingModule({
providers: [
{ provide: HttpClient, useValue: httpClientMock }, // Used for jwtHttp
{ provide: HttpBackend, useValue: httpBackendMock }, // Used for cleanHttp
{
provide: PermissionsStoreService,
useValue: {
// create a real computed signal parameter to use with changing
// effect() subscriptions
expiredStatus: computed(() => signalMock()),
},
});
If I try to force the correct types for a Mock, I can get the test to run, but the value won't report as changed via Testbed.flushEffects() which is new in Angular 17.2.0.
expiredStatusMockSignal = signal(true);
expiredStatusMockComputed = computed(() => expiredStatusMockSignal());
// Define the type for a Signal that emits boolean values
type BooleanSignal = Signal<boolean>;
// Assuming permissionsStoreServiceMock is a mock object and signalMock is a mock function
const permissionsStoreServiceMock = {
expiredStatus: jasmine.createSpy("expiredStatus") as jasmine.Spy<() => BooleanSignal>,
};
// Create a spy on the expiredStatus method and provide a fake implementation
permissionsStoreServiceMock.expiredStatus.and.callFake(() => {
// If signalMock is a function that returns a value, you can return that value here
return expiredStatusMockComputed as BooleanSignal;
});
I have tried a dozen version of this test, and I think I need to address the injection context, when the constructor for AuthService is initially run, but I don't know how.
it("should react to token expiration", () => {
// Replace with your specific assertion logic
const tokenRefreshCalled = spyOn(authService, "refreshToken").and.callFake(() => {
// Do nothing or perform simple actions, depending on your needs
console.debug("Called the faker...");
return of(TokenStatus.Valid); // Or some mock response if needed
});
expiredStatusMockSignal.update(() => false);
// flush the signal change
TestBed.flushEffects();
authService.expiredStatus$.subscribe((next) => console.log("Expired Status: ", next));
console.log("starting shit show....");
expiredStatusMockSignal.update(() => true);
// flush the signal change
TestBed.flushEffects();
console.log("Status: ", expiredStatusMockSignal());
// Assert that the component reacted as expected
expect(tokenRefreshCalled).toHaveBeenCalled();
});