41
loading...
This website collects cookies to deliver better user experience
Note: You can use this API and functionality in NgRx v5 and v6 using the standalone pngrx-mockstore package.
StoreModule
in the testing module configuration. The StoreModule creates a store with the initial state defined in the store’s reducer. To condition the desired state for a given test case, you could have to dispatch several actions.MockStore
class provides a simpler way to condition NgRx state in unit tests. You provide an initial default state, then update the state using setState(<nextState>)
.// NgRx v7.3.0
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
constructor(private store: Store<fromAuth.State>) {}
canActivate(): Observable<boolean> {
return this.store.pipe(
select(fromAuth.getLoggedIn),
map(authed => {
if (!authed) {
this.store.dispatch(new AuthApiActions.LoginRedirect());
return false;
}
return true;
}),
take(1)
);
}
}
AuthGuard
selects getLoggedIn
from the store. If the latest getLoggedIn
is true, a LoginRedirect
action is dispatched and the function returns false. If the latest getLoggedIn is false, it returns true.StoreModule
, which requires the test to dispatch a LoginSuccess
action to condition the getLoggedIn
selector to return true:// NgRx v7.3.0
describe('Auth Guard', () => {
let guard: AuthGuard;
let store: Store<any>;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot({
...fromRoot.reducers,
auth: combineReducers(fromAuth.reducers),
}),
],
});
store = TestBed.get(Store);
spyOn(store, 'dispatch').and.callThrough();
guard = TestBed.get(AuthGuard);
});
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 user: any = {};
const action = new AuthApiActions.LoginSuccess({ user });
store.dispatch(action);
const expected = cold('(a|)', { a: true });
expect(guard.canActivate()).toBeObservable(expected);
});
});
MockStore
:// Future version of example-app using MockStore
import { provideMockStore, MockStore } from '@ngrx/store/testing';
describe('Auth Guard', () => {
let guard: AuthGuard;
let store: MockStore<fromAuth.State>;
const initialState = {
auth: {
loginPage: {} as fromLoginPage.State,
status: {
user: null,
},
},
} as fromAuth.State;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [AuthGuard, provideMockStore({ initialState })],
});
store = TestBed.get(Store);
guard = TestBed.get(AuthGuard);
});
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', () => {
store.setState({
...initialState,
auth: {
loginPage: {} as fromLoginPage.State,
status: {
user: {
name: 'John',
},
},
},
});
const expected = cold('(a|)', { a: true });
expect(guard.canActivate()).toBeObservable(expected);
});
});
MockStore
using the same type assertion that is used when declaring the Store in the AuthGuard (fromAuth.State
).fromAuth.State
extends
fromRoot.State
and our tests only depend on the the user
attribute, we can cast everything else.MockStore
using provideMockStore
, passing in the initialState
created in the previous step.Store
inside the test.setState
.withLatestFrom
operator and the StoreModule
.@Effect()
example$ = this.actions$.pipe(
ofType(ActionTypes.ExampleAction),
withLatestFrom(this.store.pipe(
select(fromExample.getShouldDispatchActionOne)
)),
map(([action, shouldDispatchActionOne]) => {
if (shouldDispatchActionOne) {
return new ActionOne();
} else {
return new ActionTwo();
}
})
);
TestBed.get(<effect>)
had been called, making it difficult to test different values selected by getShouldDispatchActionOne
in the above snippet. The three common workarounds were:SpyOn
to mock the return value of state.select(…)
: spyOn(store, 'select').and.returnValue(of(initialState))
. However, select
is now an RxJs operator. ❌TestBed.get(<effect>)
from beforeEach
into each individual test after the state is conditioned appropriately. 😐withLatestFrom
using the MockStore:addBookSuccess$
, to the NgRx example-app’s BookEffects
. When a new book is successfully added, we’ll select the books the user now has in their collection the store, then display an alert with a different message depending on the quantity:@Injectable()
export class BookEffects {
@Effect({ dispatch: false })
addBookSuccess$ = this.actions$.pipe(
ofType(CollectionApiActionTypes.AddBookSuccess),
withLatestFrom(this.store.select(fromBooks.getCollectionBookIds)),
tap(([action, bookCollection]) => {
if (bookCollection.length === 1) {
window.alert('Congrats on adding your first book!')
} else {
window.alert('You have added book number ' + bookCollection.length);
}
})
);
// search$ effect deleted for simplicity
constructor(
private actions$: Actions<FindBookPageActions.FindBookPageActionsUnion>,
// ...
private store: Store<fromBooks.State>
) {}
}
MockStore
to condition the state, allowing us to test each of the two cases:import * as fromBooks from '@example-app/books/reducers';
import * as fromSearch from '@example-app/books/reducers/search.reducer';
import * as fromChildBooks from '@example-app/books/reducers/books.reducer';
// Omitting autoimports
describe('BookEffects', () => {
let effects: BookEffects;
let actions$: Observable<any>;
let store: MockStore<fromBooks.State>;
const initialState = {
books: {
search: {} as fromSearch.State,
books: {} as fromChildBooks.State,
collection: {
loaded: true,
loading: false,
ids: ['1']
}
}
} as fromBooks.State;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
BookEffects,
{
provide: GoogleBooksService,
useValue: { searchBooks: jest.fn() },
},
provideMockActions(() => actions$),
provideMockStore({ initialState }),
],
});
effects = TestBed.get(BookEffects);
actions$ = TestBed.get(Actions);
store = TestBed.get(Store);
spyOn(window, 'alert');
});
describe('addBookSuccess$', () => {
it('should print congratulatory message when adding '
+ 'the first book', (done: any) => {
const action = new AddBookSuccess(generateMockBook());
actions$ = of(action);
effects.addBookSuccess$.subscribe(() => {
expect(window.alert)
.toHaveBeenCalledWith(
'Congrats on adding your first book!'
);
done();
});
});
it('should print number of books after adding '
+ 'the first book', (done: any) => {
store.setState({
...initialState,
books: {
search: {} as fromSearch.State,
books: {} as fromChildBooks.State,
collection: {
loaded: true,
loading: false,
ids: ['1', '2']
}
}
});
const action = new AddBookSuccess(generateMockBook());
actions$ = of(action);
effects.addBookSuccess$.subscribe(() => {
expect(window.alert)
.toHaveBeenCalledWith(
'You have added book number 2'
);
done();
});
});
});
});
AuthGuard
Example:MockStore
using the same type assertion that is used when declaring the Store in the BookEffects (fromBooks.State
).fromBooks.State
extends
fromRoot.State
and our tests only depend on the the ids attribute, we can cast everything else.MockStore
using provideMockStore
, passing in the initialState
created in the previous step.Store
inside the test.setState
.