Reputation: 9692
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).
Upvotes: 28
Views: 42744
Reputation: 41
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
Using the "overrideSelector" you can overwrite a selector you created and make it return a mocked value. For example
store = TestBed.inject(MockStore);
store.overrideSelector(getState, mockValue);
Upvotes: 4
Reputation: 810
Using the overrideSelector worked for me. This video helped me solve the problem https://www.youtube.com/watch?v=NOT-nJLDnyg
imports { ... } from '...';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
describe('MyComponent', () => {
let store: MockStore<AppState>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ MyComponent ],
imports: [
...
],
providers: [
...
provideMockStore({initialState}),
],
schemas: [NO_ERRORS_SCHEMA],
})
.compileComponents();
fixture = TestBed.createComponent(MyComponent);
store = TestBed.inject(MockStore);
component = fixture.componentInstance;
store.overrideSelector(mySelector, {
id: 0,
name: 'test',
value: 50,
})
fixture.detectChanges();
spyOn(store, 'dispatch').and.callFake(() => {});
});
});
Upvotes: 4
Reputation: 3723
The best solution I've found is using a switch statement to return the data you want for each selector. The solution @vince provides only works when mocking one select request.
For example:
jest.spyOn(store, "select").mockImplementation((selector) => {
switch (selector) {
case selectSelectedKey:
return of(key);
case selectCountry:
return of(CANADA_MOCK);
}
return EMPTY;
});
Upvotes: 1
Reputation: 541
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
Upvotes: 6
Reputation: 3478
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}));
});
Upvotes: 3
Reputation: 1741
You could use something like that:
spyOn(store, 'select').and.callFake(selectFake);
function pipeFake(op1: OperatorFunction<UsersState, any>): Observable<any> {
if (op1.toString() === fromStore.getLoading.toString()) {
return of(true);
}
if (op1.toString() === fromStore.getUsers.toString()) {
return of(fakeUsers);
}
return of({});
}
Upvotes: -1
Reputation: 44
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.
Upvotes: 1
Reputation: 8306
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
});
});
Upvotes: 10