how to mock ngrx selector in a component

22,894

Solution 1

I ran into the same challenge and solved it once and for all by wrapping my selectors in services, so my components just used the service to get their data rather than directly going through the store. I found this cleaned up my code, made my tests implementation-agnostic, and made mocking much easier:

mockUserService = {
  get users$() { return of(mockUsers); },
  get otherUserRelatedData$() { return of(otherMockData); }
}

TestBed.configureTestingModule({
  providers: [{ provide: UserService, useValue: mockUserService }]
});

Before I did that however, I had to solve the issue in your question.

The solution for you will depend on where you are saving the data. If you are saving it in the constructor like:

constructor(private store: Store) {
  this.users$ = store.select(getUsers);
}

Then you will need to recreate the test component every time you want to change the value returned by the store. To do that, make a function along these lines:

const createComponent = (): MyComponent => {
  fixture = TestBed.createComponent(MyComponent);
  component = fixture.componentInstance;
  fixture.detectChanges();
  return component;
};

And then call that after you change the value of what your store spy returns:

describe('test', () => {
  it('should get users from the store', () => {
    const users: User[] = [{username: 'BlackHoleGalaxy'}]; 
    store.select.and.returnValue(of(users));
    const cmp = createComponent();
    // proceed with assertions
  });
});

Alternatively, if you are setting the value in ngOnInit:

constructor(private store: Store) {}
ngOnInit() {
  this.users$ = this.store.select(getUsers);
}

Things are a bit easier, as you can create the component once and just recall ngOnInit every time you want to change the return value from the store:

describe('test', () => {
  it('should get users from the store', () => {
    const users: User[] = [{username: 'BlackHoleGalaxy'}]; 
    store.select.and.returnValue(of(users));
    component.ngOnInit();
    // proceed with assertions
  });
});

Solution 2

I created a helper like that:

class MockStore {
        constructor(public selectors: any[]) {
        }

        select(calledSelector) {
          const filteredSelectors = this.selectors.filter(s => s.selector === calledSelector);
          if (filteredSelectors.length < 1) {
            throw new Error('Some selector has not been mocked');
          }
          return cold('a', {a: filteredSelectors[0].value});
        }
 }

And now my tests look like this:

  const mock = new MockStore([
    {
      selector: selectEditMode,
      value: initialState.editMode
    },
    {
      selector: selectLoading,
      value: initialState.isLoading
    }
  ]);

  it('should be initialized', function () {
    const store = jasmine.createSpyObj('store', ['dispatch', 'select']);
    store.select.and.callFake(selector => mock.select(selector));

    const comp = new MyComponent(store);

    comp.ngOnInit();

    expect(comp.editMode$).toBeObservable(cold('a', {a: false}));
    expect(comp.isLoading$).toBeObservable(cold('a', {a: false}));
  });

Solution 3

Moving your selectors into a service will not eliminate the need to mock selectors, if you are going to test selectors themselves. ngrx now has its own way of mocking and it is described here: https://ngrx.io/guide/store/testing

Solution 4

If what you want to accomplish is to mock a state update so that your subscription to your selector receives a new value, you should use what NgRx suggests here. https://ngrx.io/guide/store/testing#using-mock-selectors

Solution 5

I also ran into this problem and using services to wrap the selectors is no option for me, too. Especially not only for testing purposes and because I use the store to replace services.

Therefore I came up with the following (also not perfect) solution:

I use a different 'Store' for each component and each different aspect. In your example I would define the following Stores&States:

export class UserStore extends Store<UserState> {}

export class LoadingStatusStore extends Store<LoadingStatusState> {}

And inject them in the User-Component:

constructor( private userStore: UserStore, private LoadingStatusStore: 
LoadingStatusStore ) {}

Mock them inside the User-Component-Test-Class:

TestBed.configureTestingModule({
  imports: [...],
  declarations: [...],
  providers: [
    { provide: UserStore, useClass: MockStore },
    { provide: LoadingStatusStore, useClass: MockStore }
  ]
}).compileComponents();

Inject them into the beforeEach() or it() test method:

beforeEach(
  inject(
    [UserStore, LoadingStatusStore],
      (
        userStore: MockStore<UserState>,
        loadingStatusStore: MockStore<LoadingStatusState>
      ) => {...}

Then you can use them to spy on the different pipe methods:

const userPipeSpy = spyOn(userStore, 'pipe').and.returnValue(of(user));
const loadingStatusPipeSpy = spyOn(loadingStatusStore, 'pipe')
  .and.returnValue(of(false));

The drawback of this method is that you still can't test more than one part of a state of a store in one test-method. But for now this works as a workaround for me.

Share:
22,894

Related videos on Youtube

BlackHoleGalaxy
Author by

BlackHoleGalaxy

Updated on March 23, 2022

Comments

  • BlackHoleGalaxy
    BlackHoleGalaxy about 2 years

    In a component, we use a ngrx selector to retrieve different parts of the state.

    public isListLoading$ = this.store.select(fromStore.getLoading);
    public users$ = this.store.select(fromStore.getUsers);
    

    the fromStore.method is created using ngrx createSelector method. For example:

    export const getState = createFeatureSelector<UsersState>('users');
    export const getLoading = createSelector(
      getState,
      (state: UsersState) => state.loading
    );
    

    I use these observables in the template:

    <div class="loader" *ngIf="isLoading$ | async"></div>
    <ul class="userList">
        <li class="userItem" *ngFor="let user of $users | async">{{user.name}}</li>
    </div>
    

    I would like to write a test where i could do something like:

    store.select.and.returnValue(someSubject)
    

    to be able to change subject value and test the template of the component agains these values.

    The fact is we struggle to find a proper way to test that. How to write my "andReturn" method since the select method is called two times in my component, with two different methods (MemoizedSelector) as arguments?

    We don't want to use real selector and so mocking a state then using real selector seems not to be a proper unit test way (tests wouldn't be isolated and would use real methods to test a component behavior).

    • Chris Bao
      Chris Bao almost 5 years
      One question, the 'returnValue' is from jasmine, right? I faced another issue is, when I use selector function in store.select. The jasmine's spy solution can't work. But if I use store.select('somefield') directly, it can work. Do you have similar issue?
    • Shadab Umer
      Shadab Umer about 3 years
      @ChrisBao currently I'm facing the same issue. Can you please share if you were able to solve this issue? Especially for state selectors.
  • BlackHoleGalaxy
    BlackHoleGalaxy about 6 years
    Thanks. Wrapping in service just for testing purpose is a non sense. And your second solutions works well EXCEPT if you have multiple select in your component. Then the store.select.and.returnValue becomes unusable because both your selects will get the same value (which may cause the app to crash at test time because an async pipe receive and treat something which is not a valid data for that place.
  • vince
    vince about 6 years
    I agree wrapping it in a service just for testing is nonsense, but you may find there are other benefits by not relying on directly injecting the store. Also, if you are setting up your tests such that you are only testing one thing at a time, you shouldn't need to worry about testing multiple values in the same unit test. That being said, the service approach does allow you to test multiple at once as you would have a getter or method per selector.
  • Erik Philips
    Erik Philips about 5 years
    Why use ngrx at all if you're going to wrap it in way the abstracts away whole CQRS pattern of ngrx. Might as well just not use it.
  • MartaGalve
    MartaGalve almost 5 years
    This way you cannot mock different selectors to different responses, only one at a time. This will only cover very few use cases.
  • MartaGalve
    MartaGalve almost 5 years
    This won't work with selectors with parameters, will it?
  • el-davo
    el-davo about 3 years
    This should be the accepted answer, it works perfectly and is from the official docs
  • Mario Petrovic
    Mario Petrovic over 2 years
    While this link may answer the question, it is better to include the essential parts of the answer here and provide the link for reference. Link-only answers can become invalid if the linked page changes. - From Review