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)
);
}
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With