DEV Community

Cover image for MockStore in NgRx v7.0
ng-conf
ng-conf

Posted on

MockStore in NgRx v7.0

John Crowson | ng-conf | Apr 2019

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 pngrx-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:

// 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)
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
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);
  });
});
Enter fullscreen mode Exit fullscreen mode
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);
  });
});
Enter fullscreen mode Exit fullscreen mode
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 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();
    }
  })
);
Enter fullscreen mode Exit fullscreen mode

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>
  ) {}
}
Enter fullscreen mode Exit fullscreen mode
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();
      });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode
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).
  2. 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 :)

For more Angular goodness, be sure to check out the latest episode of The Angular Show podcast.

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/

Top comments (12)

Collapse
 
waston_john profile image
Waston John • Edited

It's great to see the release of NgRx v7.0 and the new MockStore module, which definitely makes testing easier. Just like how Singaporean food offers a variety of flavors that cater to all kinds of tastes, NgRx's flexibility in handling state testing with tools like MockStore makes it easier for developers to manage complex states. Speaking of varieties, whether you're working on a tech project or enjoying a meal, a place like Burger King Singapore provides a fast and satisfying option, with its iconic Whopper being as consistent and reliable as a well-tested application. It’s a reminder of how a little innovation can go a long way in both software and dining.

Collapse
 
johngrant12 profile image
John Grant

Are you into sandbox simulation games where you can create and manipulate entire worlds? Worldbox is one of the best games in this genre. It offers endless creativity and experimentation. For those who love exploring additional features, mods are a great way to enhance gameplay. Check out this amazing resource for everything Worldbox-related!

Collapse
 
waston_john profile image
Waston John

That’s a great suggestion—Worldbox is definitely a fun way to let your creativity run wild! If you're someone who loves experimenting with different worlds, you might also enjoy an East African Safari by Kilidove while you're taking a break from the game. Picture this: after a day of creating and manipulating worlds in Worldbox, you could relax on a real-world safari in Tanzania, exploring the incredible wildlife and diverse landscapes. It’s the perfect blend of adventure, allowing you to immerse yourself in both virtual and real-world environments.

Collapse
 
waston_john profile image
Waston John • Edited

While discussing the simplicity and effectiveness of using MockStore in NgRx testing, I couldn't help but think about how easy it is to get distracted by technical details—just like deciding on the perfect Sonic drink! Whether you prefer a simple soda or a more elaborate slush, Sonic drink prices offer a variety of options to match your needs, just like the flexibility that MockStore provides for NgRx testing. Much like adjusting your state in a unit test, adjusting the flavors in a Sonic drink can make a world of difference. Whether you're building apps or enjoying a refreshing beverage, it's all about finding that perfect balance.

Collapse
 
merris29 profile image
Merris Smith

MockStore in NgRx v7.0 provides valuable insights into simplifying and improving the process of testing state management in Angular applications. This innovation reminds me of the 7Brew hidden menu 2025, where a wide variety of thoughtfully curated options ensures a seamless experience for customers, just as MockStore ensures developers can efficiently and effectively test their applications without unnecessary complexity.

Collapse
 
jamaica23 profile image
Jamaica Squeez

MockStore in NgRx v7.0 showcases how MockStore empowers developers to streamline and simplify the testing of state management in Angular apps. Similarly, the Spike Volleyball APK delivers a seamless gaming experience, offering users the tools to fine-tune their gameplay, just as developers can fine-tune their application testing with MockStore.

Collapse
 
jesson20 profile image
Jesson Henry

MockStore in NgRx v7.0 emphasizes how it simplifies and enhances testing for state management in Angular applications. Similarly, the Manok Na Pula APK enriches the gaming experience by adding exciting new features, providing users with a smoother and more enjoyable journey, just like MockStore improves the workflow for developers.

Collapse
 
waston_john profile image
Waston John • Edited

MockStore in NgRx v7.0 provides valuable insights into simplifying and improving the process of testing state management in Angular applications. This innovation reminds me of the warehouse renovation, where a wide variety of thoughtfully curated options ensures a seamless experience for customers, just as MockStore ensures developers can efficiently and effectively test their applications without unnecessary complexity.

Collapse
 
johngrant12 profile image
John Grant

Using MockStore in NgRx v7.0 is like strategizing your shots in 8 Ball Pool apk. Both require precision, planning, and setting things up the right way to achieve smooth execution—whether it's predicting game state changes or sinking that perfect bank shot!

Collapse
 
jessica92 profile image
Jessica Hicks

MockStore in NgRx v7.0 highlights the convenience and flexibility it brings to testing state management in Angular applications. Similarly, the Summertime Saga Mod APK old versions enhances the user experience by unlocking additional features and streamlining gameplay, much like MockStore unlocks more efficient and effective testing for developers.

Collapse
 
merris29 profile image
Merris Smith

MockStore in NgRx v7.0 sheds light on how it revolutionizes state management testing by offering developers a more efficient and user-friendly approach. Similarly, the Wink APK enhances user connectivity by streamlining interactions and offering a seamless platform, reflecting how MockStore simplifies and optimizes the development process.