Testing NgRx component-store with Jest

Testing NgRx component-store with Jest

ยท

6 min read

Working with Nx and NgRx component store, there is not much of a guide to see how to implement jest testing for components so I found this guide in one of Beeman's videos with Chau, check the video here .

In order to test faster and more efficiently we are going to use SPECTATOR with jest so we need to install the dev dependency first.

npm install -D @ngneat/spectator

Remeber to set this properties in ts.config.json in order to use jest:

"compilerOptions": {
    "esModuleInterop": true,
    "emitDecoratorMetadata": true,
    "allowJs": true,

Testing the component

We are going to test a simple component wich implements component store to fetch users and keep them in the state

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [AppStore],
})
export class AppComponent implements OnInit {
  vm$ = this.appStore.vm$;
  constructor(private appStore: AppStore) {}

  ngOnInit(): void {
    this.appStore.getUsers();
  }

  updateOne(user: any): void {
    this.appStore.updateUser(user);
  }
}

And the store looks like this:

export interface AppState {
  users: User[];
  loading: boolean;
}

const DEFAULT_STATE: AppState = {
  users: [],
  loading: false,
};

@Injectable()
export class AppStore extends ComponentStore<AppState> {
  constructor(private readonly dataService: DataService) {
    super(DEFAULT_STATE);
  }

  readonly vm$ = this.select(({ users, loading }) => ({ users, loading }));

  readonly getUsers = this.effect(trigger$ =>
    trigger$.pipe(
      tap(() => this.patchState({ loading: true })),
      switchMap(() =>
        this.dataService.getData().pipe(
          tapResponse(
            users => this.addUsers(users),
            error => console.log(error),
          ),
        ),
      ),
    ),
  );

  readonly updateUser = this.effect<User>(user$ =>
    user$.pipe(
      exhaustMap(user =>
        this.dataService.updateOne(user).pipe(
          tapResponse(
            () => this.getUsers(),
            error => console.log(error),
          ),
        ),
      ),
    ),
  );

  readonly addUsers = this.updater((state, users: User[]) => ({
    ...state,
    users,
    loading: false,
  }));
}

So, let's get our hands dirty with the spec file:

describe('AppComponent', () => {
  let spectator: Spectator<AppComponent>;
  let store: SpyObject<AppStore>;
  // createComponentFactory returns a functions wich 
  // we can execute to instaciate the component
  const createComponent = createComponentFactory({
    component: AppComponent,
    imports: [RouterTestingModule],
    mocks: [DataService],
    // componentProviders helps us to mock the store where we 
    // set the view model and its methods
    componentProviders: [
      mockProvider(AppStore, {
        vm$: of({ users: [], loading: false }),
        getUsers: jest.fn(),
        updateUser: jest.fn(),
      }),
    ],
    // With shallow set to true we isolate the component tests 
    // to not care about other nested components
    shallow: true,
  });

  beforeEach(() => {
    spectator = createComponent();
    store = spectator.inject(AppStore, true);
  });

  describe('initialize', () => {
    it('should create the component', () => {
      expect(spectator.component).toBeTruthy();
      expect(spectator.component.vm$).toBe(store.vm$);
    });

    it('should call store.getUsers on init', () => {
      expect(store.getUsers).toHaveBeenCalled();
    });
  });

  it('should call store.updateUser on updateOne', () => {
    const user: User = {
      id: 1,
      name: 'foo',
      username: 'foo',
      email: 'foo'
    };
    spectator.component.updateOne(user);
    expect(store.updateUser).toHaveBeenCalledWith(user);
  });
});

Testing the store

In order to make it easier to test observables and rxjs in general we are going to use another library

npm install -D @hirez_io/observer-spy

And the testing file:

import { subscribeSpyTo } from '@hirez_io/observer-spy';
import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator/jest';
import { of } from 'rxjs';
import { AppStore } from './app.store';
import { User } from './models/User';
import { DataService } from './services/data.service';

describe('WorkflowListStore', () => {
  let spectator: SpectatorService<AppStore>;
  let vm$: typeof AppStore.prototype['vm$'];

  const getVm = (
    partial: Partial<{
      users: User[];
      loading: boolean;
    }> = {},
  ) => ({
    users: [],
    loading: false,
    ...partial,
  });

  const createService = createServiceFactory({
    service: AppStore,
    mocks: [DataService],
  });

  beforeEach(() => {
    spectator = createService();
    vm$ = spectator.service.vm$;
  });

  describe('initialize', () => {
    it('should create', () => {
      expect(spectator.service).toBeTruthy();
      expect(vm$).toBeTruthy();
    });

    it('should vm$ emit default value', () => {
      const observerSpy = subscribeSpyTo(vm$);
      expect(observerSpy.getValues()).toEqual([getVm()]);
    });
  });

  describe('effects', () => {
    let service: SpyObject<DataService>;
    const user: User = {
      id: 1,
      name: 'foo',
      username: 'foo',
      email: 'foo',
      },
    };

    const mockedUsers = of<User[]>([user, user]);

    beforeEach(() => {
      service = spectator.inject(DataService);
    });

    describe('loadUsersEffect', () => {
      it('should call service and get users properly', () => {
        const observerSpy = subscribeSpyTo(vm$);
        service.getData.mockReturnValueOnce(mockedUsers);

        spectator.service.getUsers();

        expect(service.getData).toHaveBeenCalled();
        expect(observerSpy.getLastValue()).toEqual(getVm({ users: [user, user], loading: false }));
      });
    });

    it('should call service methods properly on update user effect', () => {
      const observerSpy = subscribeSpyTo(vm$);
      service.updateOne.mockReturnValueOnce(of(user));

      const updateUserInput: User = user;
      spectator.service.updateUser(updateUserInput);

      expect(observerSpy.getLastValue()).toEqual(getVm({ users: [], loading: true }));
      expect(service.updateOne).toHaveBeenCalledWith(updateUserInput);
      expect(service.getData).toHaveBeenCalled();
    });
  });
});

In the video sourced before you will find more complex examples with more explanation of use cases testing component store.