MockStore in NgRx v7.0
NgRx v7.0 included the released of a new @ngrx/store/testing
module that features a mock Store to be used in testing NgRx applications. The module was introduced in #1027 with some documentation following in #1591.
Note: You can use this API and functionality in NgRx v5 and v6 using the standalone ngrx-mockstore package.
Currently, the documentation is light and doesn’t include a complete working code sample. I’ll provide two examples that should help clear things up.
Existing: StoreModule
It has been possible to condition the NgRx store in a unit test by providing the 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.
New: MockStore
The 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>)
.
Let’s see how MockStore can simplify an existing test suite:
Testing Auth Guard Example
The NgRx example-app contains an AuthGuard that provides us a simple example of using the MockStore:
The 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.
The existing AuthGuard test uses the StoreModule
, which requires the test to dispatch a LoginSuccess
action to condition the getLoggedIn
selector to return true:
Let’s refactor the same tests to condition the store’s state without actions using MockStore
:
Here are the steps:
- Line 6: Declare a
MockStore
using the same type assertion that is used when declaring the Store in the AuthGuard (fromAuth.State
). - Line 7: Create an initial state conforming to the same state interface that was asserted on line 6. This will be the default state for all tests. Since
fromAuth.State
extends
fromRoot.State
and our tests only depend on the theuser
attribute, we can cast everything else. - Line 19: Provide the
MockStore
usingprovideMockStore
, passing in theinitialState
created in the previous step. - Line 22: Inject the
Store
inside the test. - Line 31: To condition a different state, use
setState
.
Testing Effect + withLatestFrom Example
I came across NgRx issue #414 which describes difficulty testing effects that incorporate state using the 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();
}
})
);
The effect’s injected state couldn’t be changed after 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:
- Use Jasmine’s
SpyOn
to mock the return value ofstate.select(…)
:spyOn(store, 'select').and.returnValue(of(initialState))
. However,select
is now an RxJs operator. ❌ - Move
TestBed.get(<effect>)
frombeforeEach
into each individual test after the state is conditioned appropriately. 😐 - Provide a mockStore (hey, don’t we have one of those now?). 😀
Let’s see how we can test effects that use withLatestFrom
using the MockStore:
Let’s add a new effect, 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:
We can use the MockStore
to condition the state, allowing us to test each of the two cases:
Here are the steps, similar to those in the AuthGuard
Example:
- Line 9: Declare a
MockStore
using the same type assertion that is used when declaring the Store in the BookEffects (fromBooks.State
). - Line 10: Create an initial state conforming to the same state interface that was asserted on line 9. This will be the default state for all tests. Since
fromBooks.State
extends
fromRoot.State
and our tests only depend on the theids
attribute, we can cast everything else. - Line 32: Provide the
MockStore
usingprovideMockStore
, passing in theinitialState
created in the previous step. - Line 38: Inject the
Store
inside the test. - Line 59: To condition a different state, use
setState
.
Thanks for reading! You can follow me on Twitter @john_crowson :)
For more Angular goodness, be sure to check out the latest episode of The Angular Show podcast.
EnterpriseNG is coming November 4th & 5th, 2021.
Come hear top community speakers, experts, leaders, and the Angular team present for 2 stacked days on everything you need to make the most of Angular in your enterprise applications.
Topics will be focused on the following four areas:
• Monorepos
• Micro frontends
• Performance & Scalability
• Maintainability & Quality
Learn more here >> https://enterprise.ng-conf.org/