I have an @Effect
that uses a MemoizedSelector
to grab an item from the redux store and mergeMap
it with the payload of an Action. The effect works just fine, however setting up the Jest tests for this has proven difficult as I cannot seem to mock the return value of the selector due to select
being a declared function that's imported (from '@ngrx/store') and used in the effect and the selector itself being an imported function as well. I'm grasping at straws now.
How can I write a Unit Test to test an NGRX effect that makes use of a store selector?
"@ngrx/store": "^7.4.0",
"rxjs": "^6.2.2"
provideMockStore({
initialState
})
provideMockStore comes in from '@ngrx/store/testing';
where initial state was both my actual initialState and a state that contains the exact structure/item I'm trying to select
using different types of MockStore
's from various SO questions/answers as well as different blog posts approaches
attempting to mock the selector using <selector>.projector(<my-mock-object>)
(straw-grasping here, I'm pretty sure this would be used in isolated testing of the selector not the Effect)
The Effect itself:
@Effect()
getReviewsSuccess$ = this.actions$.pipe(
ofType<ProductActions.GetReviewsSuccess>(
ProductActions.ProductActionTypes.GET_REVIEWS_SUCCESS
),
mergeMap(() => this.reduxStore.pipe(select(selectProduct))),
map(({ product_id }) => product_id),
map(product_id => new ProductActions.GetReviewsMeta({
product_id,
}))
);
The Spec:
......
let effects: ProductEffects;
let facade: Facade;
let actions$: Observable<any>;
let store$: Observable<State>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule,
// ^ I've also tried using StoreModule.forRoot(...) here to configure
// it in similar fashion to the module where this effect lives
],
providers: [
ProductEffects,
provideMockActions(() => actions$),
{
provide: Facade,
useValue: facadeServiceMock,
},
ResponseService,
provideMockStore({
initialState
})
// ^ also tried setting up the test with different variations of initialState
],
});
......
it('should return a GetReviewsMeta on successful GetReviewsSuccess', () => {
const reviews = {...reviewListMock};
const { product_id } = {...productMockFull};
const action = new ProductActions.GetReviewsSuccess({
reviews
});
const outcome = new ProductActions.GetReviewsMeta({
product_id
});
actions$ = hot('-a', { a: action });
// store$ = cold('-c', { c: product_id });
// not sure what, if anything I need to do here to mock select(selectProduct)
const expected = cold('-b', { b: outcome });
expect(effects.getReviewsSuccess$).toBeObservable(expected);
});
The Selector selectProduct
:
export const getProduct = ({product}: fromProducts.State) => product;
export const getProductState = createFeatureSelector<
fromProducts.State
>('product');
export const selectProduct = createSelector(
getProductState,
getProduct,
);
I expect the test to pass but instead I keep getting the following error
● Product Effects › should return a GetReviewsMeta on successful GetReviewsSuccess
expect(received).toBeNotifications(expected)
Expected notifications to be:
[{"frame": 10, "notification": {"error": undefined, "hasValue": true, "kind": "N", "value": {"payload": {"product_id": 2521}, "type": "[Reviews] Get Reviews Meta"}}}]
But got:
[{"frame": 10, "notification": {"error": [TypeError: Cannot read property 'product_id' of undefined], "hasValue": false, "kind": "E", "value": undefined}}]
Clearly the MemoizedSelector
(selectProduct) doesn't know what the Product Object is that should be in the store (but doesn't seem to be whether I inject an initialState
that has it or not) and can't get the product_id
of the Product because I didn't set this up correctly in the beforeEach
or in the spec itself...
Testing NgRx facade using override/MockStore I recommend you use MockStore (with/without overrideSelector) to mocking the store state. If you want to involve the selectors in the test (integtration testing them), you should use MockStore : You set the state using the setState method from MockStore .
Selectors are basically the NgRx change detection mechanism. When a state is updated, NgRx doesn't do any comparison between old state and the new state to figure out what observables to trigger. It simply sends a new state through all top level selectors.
We got this covered in the ngrx.io docs. Note that the syntax is for NgRx 8 but the same ideas count for NgRx 7.
addBookToCollectionSuccess$ = createEffect(
() =>
this.actions$.pipe(
ofType(CollectionApiActions.addBookSuccess),
withLatestFrom(this.store.pipe(select(fromBooks.getCollectionBookIds))),
tap(([, bookCollection]) => {
if (bookCollection.length === 1) {
window.alert('Congrats on adding your first book!');
} else {
window.alert('You have added book number ' + bookCollection.length);
}
})
),
{ dispatch: false }
);
it('should alert number of books after adding the second book', () => {
store.setState({
books: {
collection: {
loaded: true,
loading: false,
ids: ['1', '2'],
},
},
} as fromBooks.State);
const action = CollectionApiActions.addBookSuccess({ book: book1 });
const expected = cold('-c', { c: action });
actions$ = hot('-a', { a: action });
expect(effects.addBookToCollectionSuccess$).toBeObservable(expected);
expect(window.alert).toHaveBeenCalledWith('You have added book number 2');
});
});
Make sure that your state has the same structure as in the redux devtools.
NgRx 8 also provides a way to mock selectors, so it isn't needed to set up the whole state tree for a single test - https://next.ngrx.io/guide/store/testing#using-mock-selectors.
describe('Auth Guard', () => {
let guard: AuthGuard;
let store: MockStore<fromAuth.State>;
let loggedIn: MemoizedSelector<fromAuth.State, boolean>;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [AuthGuard, provideMockStore()],
});
store = TestBed.get(Store);
guard = TestBed.get(AuthGuard);
loggedIn = store.overrideSelector(fromAuth.getLoggedIn, false);
});
it('should return false if the user state is not logged in', () => {
const expected = cold('(a|)', { a: false });
expect(guard.canActivate()).toBeObservable(expected);
});
it('should return true if the user state is logged in', () => {
const expected = cold('(a|)', { a: true });
loggedIn.setResult(true);
expect(guard.canActivate()).toBeObservable(expected);
});
});
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