How to test an injected Angular 17 computed signal effect that uses toObservable()?

304 Views Asked by At

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();
    });
0

There are 0 best solutions below