how to mock ngrx selector in a component
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.
Related videos on Youtube
BlackHoleGalaxy
Updated on March 23, 2022Comments
-
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 ngrxcreateSelector
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 almost 5 yearsOne 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 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 about 6 yearsThanks. 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 thestore.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 about 6 yearsI 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 about 5 yearsWhy 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 almost 5 yearsThis way you cannot mock different selectors to different responses, only one at a time. This will only cover very few use cases.
-
MartaGalve almost 5 yearsThis won't work with selectors with parameters, will it?
-
el-davo about 3 yearsThis should be the accepted answer, it works perfectly and is from the official docs
-
Mario Petrovic over 2 yearsWhile 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