Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how to mock ngrx selector in a component

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).

like image 948
BlackHoleGalaxy Avatar asked Mar 14 '18 21:03

BlackHoleGalaxy


People also ask

How do you test NgRx reducers?

Test a reducer Reducers are so simple to test as they are pure functions. We just need to call the reducer function and pass in a fake piece of state and an action and then check the new state slice returns.

What are props in NgRx?

The props method is used to define any additional metadata needed for the handling of the action. Action creators provide a consistent, type-safe way to construct an action that is being dispatched. Creating actions for liking and disliking a photo could look like this: // src/app/store/photo.


2 Answers

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
  });
});
like image 55
vince Avatar answered Oct 01 '22 01:10

vince


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}));
  });
like image 22
Piotr Korlaga Avatar answered Oct 01 '22 01:10

Piotr Korlaga