Search code examples
angularunit-testingsignalstoryjest-auto-spies

How to mock an angular class with signals?


I've an angular web app. I'm trying to test some services, that have other services as depenency. I need to mock those others services to test my service. I'm using Jest+auto-jest-spies to mock my classes, but I'm open to other suggestions.

Here is an example of class that I'm trying to mock(a store made with signalstory) :

import { computed, Injectable } from '@angular/core';
import { ImmutableStore, useDevtools } from 'signalstory';
import { AccountStateModel } from './account-state.model';

@Injectable({ providedIn: 'root' })
export class AccountStore extends ImmutableStore<AccountStateModel> {
  constructor() {
    super({
      plugins: [useDevtools()],
      initialState: {
        isInitialized: false,
        email: null,
        accessToken: null,
        name: null,
        tokenExpiration: null,
        userId: null,
        permissions: null,
      },
    });
  }

  //Queries
  public get isLoggedIn() {
    return computed(() => !!this.state().userId);
  }
  public get userInfo() {
    return this.state;
  }

  // Commands
  //...
}

I'm trying to mock it like this:

describe('PermissionGuard', () => {
  let storeMock!: Spy<AccountStore>;
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [
        { provide: AccountStore, useValue: createSpyFromClass(AccountStore, { gettersToSpyOn: ['isLoggedIn'] }) },
        PermissionGuard,
      ],
    }).compileComponents();
    storeMock = TestBed.inject<any>(AccountStore);
  });

it('should test the user access', async () => {
  //Arrange
  storeMock.isLoggedIn.mockReturnValue(false);

  //Act
  //...

  //Assert
  //...

  });
});

But the isLoggedIn is not recognized as a getter, I guess because it's a getter of a "function"(signal).

So what can I do to mock this class? I also want to make sure a signal has been accessed.


Solution

  • Essentially, the problem is that the getter function returns a signal, which is by itself a function. To keep all the characteristics of the signal intact, you could do something like this instead:

    
    describe('PermissionGuard', () => {
      let storeMock: Spy<AccountStore>;
      let permissionGuard: PermissionGuard;
      beforeEach(() => {
        TestBed.configureTestingModule({
          providers: [
            {
              provide: AccountStore,
              useValue: createSpyFromClass(AccountStore, {
                methodsToSpyOn: ['isLoggedIn'],
              }),
            },
            PermissionGuard,
          ],
        }).compileComponents();
        storeMock = TestBed.inject(AccountStore) as Spy<AccountStore>;
        permissionGuard = TestBed.inject(PermissionGuard);
      });
    
      it('should test the user access', () => {
        //Arrange
        storeMock.isLoggedIn.mockImplementation(signal(true));
    
        //Act
        const result = permissionGuard.isValid();
    
        //Assert
        expect(result).toBe(true);
        expect(storeMock.isLoggedIn).toHaveBeenCalledTimes(1);
        expect(storeMock.isLoggedIn).toHaveBeenCalledWith();
      });
    });
    
    

    Assuming here that PermissionGuard is defined like

    
    @Injectable({ providedIn: 'root' })
    export class PermissionGuard {
      constructor(private readonly account: AccountStore) {}
    
      isValid() {
        return this.account.isLoggedIn();
      }
    }