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