Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mock ngrx store selectors with parameters in unit tests (Angular)

I am trying to write unit tests for a service in Angular. I want to mock the store.select function of ngrx, so I can test how let's say, a service, reacts to different values returned by the store selectors. I want to be able to mock each selector individually.

My main problem is how to mock parametrised selectors.

I have previously used a BehaviourSubject that I map to the select function, but this doesn't allow you to return different values for different selectors. It is not readable because it is not obvious what selector you are mocking.

Opt 1: Mock Store using subject: impossible to know which selector the subject corresponds to, can't return different values for different selectors.

// service.spec.ts

const selectSubject = new BehaviourSubject(null);
class MockStore {
    select = () => selectSubject;
}

Opt 2: Mock Store using switch: works for different selectors, but cannot make it work when the selectors have parameters.

// service.spec.ts

    // This works well but how can I make it work with selectors with parameters??
    const firstSubject = new BehaviourSubject(null);
    const secondSubject = new BehaviourSubject(null);
    class MockStore {
        select = (selector) => {
            switch (selector): {
                case FirstSelector: {
                    return firstSubject;
                }
                case SecondSelector: {
                    return secondSubject;
                }
             }
        };
    }

    describe('TestService', () => {
        let service: TestService;
        let store: Store<any>;

        beforeEach(() => {
            TestBed.configureTestingModule({
              providers: [
                TestService,
                { provide: Store, useClass: MockStore }
              ],
            });

            service = TestBed.get(TestService);
            store = TestBed.get(Store);
          });

          it('should do X when first selector returns A and second selector returns B', () => {
            firstSelectorSubject.next(A);
            secondSelectorSubject.next(B);

        // Write expectation
          });
    });

Service method with parametrized selector I want to mock, so I can test getUserName with different id values

getUserName(id: string): Observable<string> {
    return this.store.select(getUser(id)).pipe(
      filter(user => user !== null),
      map(user => user.fullName)
    );
  }
like image 900
MartaGalve Avatar asked Apr 17 '19 23:04

MartaGalve


2 Answers

I've been working through a similar issue for awhile now and think i've found a way to make it work.

With the selector of

export const getItemsByProperty = (property: string, value: any) => createSelector(getAllItems, (items: ItemObj[]) => items.filter((item) => item[property] == value));

and where

export const getAllItems = createSelector(getState, (state) => selectAll(state.items));

in my components unit test file i override the selector for the getItemsByProperty's underlying selector call, getAllItems, with data and then expect the filtered data in my tests. If what you want to return changes, then just update the result of getAllItems.

like image 127
A Hutch Avatar answered Oct 29 '22 22:10

A Hutch


Why overrideSelector does not work

The store method overrideSelector from @ngrx/store/testing works great for selectors without parameters but does not work for mocking parameterised/factory selectors like this one:

const getItem = (itemId) => createSelector(
  getItems,
  (items) => items[itemId]
);

A new function gets created for every call to the factory function so the test class and the real class will create two separate functions and thus overrideSelector will not be able to match the functions calls.

Use spy methods

To mock factory selectors we can instead use spy methods in test frameworks like jest or jasmine.

Code example for jest:

import * as ItemSelectors from '../selectors/item.selectors';

...

const mockItem = { someProperty: 1 };

jest.spyOn(ItemSelectors, 'getItem').mockReturnValue(
  createSelector(
    (v) => v,
    () => mockItem
  )
);

For Jasmine the corresponding spy call would be something like:

  spyOn(ItemSelectors, 'getItem').and.returnValue(...);

Memoize the factory function

A different approach could be to memoize the factory function (ie getItem) so that the same function will always be returned for the same input arguments (e.g. by using memoize in lodash). Then it will be possible to use overrideSelector. However, be aware of that this builds a cache which continues to grow every time getItem gets called which can cause memory related performance problems.

like image 23
Tobias Lindgren Avatar answered Oct 29 '22 21:10

Tobias Lindgren