MockStore in NgRx v7.0

Tags: ngrx, angular, typescript, javascript
Published: 2021-07-12 18:30:49

John Crowson | ng-conf | Apr 2019

[NgRx](https://ngrx.io/) 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](https://github.com/ngrx/platform/pull/1027) with some documentation following in [#1591](https://github.com/ngrx/platform/pull/1591).

> _Note: You can use this API and functionality in NgRx v5 and v6 using the standalone [pngrx-mockstore](https://www.npmjs.com/package/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](https://github.com/ngrx/platform/tree/master/projects/example-app) contains an AuthGuard that provides us a simple example of using the MockStore:

```
// 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)
    );
  }
}
```
###### [auth-guard.service.ts](https://gist.github.com/jtcrowson/5653c6cb9fb64cc52066e81dc818742d#file-auth-guard-service-ts) hosted by GitHub

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:

```
// 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);
  });
});
```
###### [auth-guard.service.spec.ts](https://gist.github.com/jtcrowson/fdd258175821b344cd00cc7b2fc3f19f#file-auth-guard-service-spec-ts) hosted by GitHub

Let’s refactor the same tests to condition the store’s state without actions using `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);
  });
});
```
###### [auth-guard.service.spec.ts](https://gist.github.com/jtcrowson/ed4881e111a4d36a81cbc9ba03acf689#file-auth-guard-service-spec-ts) hosted by GitHub

Here are the steps:

1. Line 6: Declare a `MockStore` using the same type assertion that is used when declaring the Store in the AuthGuard (`fromAuth.State`).
2. 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 the `user` attribute, we can cast everything else.
3. Line 19: Provide the `MockStore` using `provideMockStore`, passing in the `initialState` created in the previous step.
4. Line 22: Inject the `Store` inside the test.
5. Line 31: To condition a different state, use `setState`.

---

### Testing Effect + withLatestFrom Example
I came across [NgRx issue #414](https://github.com/ngrx/platform/issues/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:

1. Use Jasmine’s `SpyOn` to mock the return value of `state.select(…)`: `spyOn(store, 'select').and.returnValue(of(initialState))`. However, `select` is now an RxJs operator. ❌
2. Move `TestBed.get(<effect>)` from `beforeEach` into each individual test after the state is conditioned appropriately. 😐
3. 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:

```
@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>
  ) {}
}
```
###### [book.effects.ts](https://gist.github.com/jtcrowson/191c435185b2dbd46bd3ca6335ae67e7#file-book-effects-ts) hosted by GitHub

We can use the `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();
      });
    });
  });
});
```
###### [book.effects.spec.ts](https://gist.github.com/jtcrowson/34c745d736c02eaa82f9e9e0d32c4fde#file-book-effects-spec-ts) hosted by GitHub

Here are the steps, similar to those in the `AuthGuard` Example:

1. Line 9: Declare a `MockStore` using the same type assertion that is used when declaring the Store in the BookEffects (`fromBooks.State`).
3. 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 the ids attribute, we can cast everything else.
3. Line 32: Provide the `MockStore` using `provideMockStore`, passing in the `initialState` created in the previous step.
4. Line 38: Inject the `Store` inside the test.
5. Line 59: To condition a different state, use `setState`.

---

Thanks for reading! You can follow me on Twitter [@john_crowson](https://twitter.com/john_crowson) :)

For more Angular goodness, be sure to check out the latest episode of [The Angular Show podcast](https://www.spreaker.com/show/angular-show).

## ng-conf: Join us for the Reliable Web Summit

Come learn from community members and leaders the best ways to build reliable web applications, write quality code, choose scalable architectures, and create effective automated tests. Powered by ng-conf, join us for the Reliable Web Summit this August 26th & 27th, 2021.
https://reliablewebsummit.com/