How To Unit Test Angular Components With Fake NgRx TestStore

(Original 📷 by Jens Johnsson)

What are we going to learn?

  1. How to implement NgRx TestStore
  2. When to use NgRx TestStore
  3. How to set up TestStore in your tests
  4. How to test UI of a component
  5. How to test dispatched actions of a component

But first a little bit of …

Context

Maybe you are aware of Angular NgRx Material Starter project. It’s a community effort to provide up to date seed that focuses on implementing all known best practices.

One of the last missing pieces of the puzzle was a good test coverage that could demonstrate best practices in testing of different types of entities that are needed in every Angular project including services, view components, container components, reducers, …

Instead of adding tests myself, I have chosen a different strategy. I have created a set of issues that describe what kind of tests would be nice to have in a project.

To my great surprise this call for help was answered by @shootermv (and others) with his PR adding tests to the todos component.

One of the challenges we were facing during the review was to find the best way to initialize component state before running actual tests. We iterated on multiple solutions including custom testMetaReducer to reset initial store state during the app startup.

It worked but it still didn't feel quite right. There was still a big amount of the dependencies including CoreModule that needed to be imported to get the tests up and running. Besides that, the tests were tightly coupled to the implementation of the corresponding reducer just to be able to initialize desired state.

Luckily, there was a better way!

Introducing NgRx TestStore

The TestStore has a very minimalistic implementation which comfortably fits into 15 lines of code while being very generous with the white space.

As we can see, TestStore uses generics with type variable <T> so we can provide type information for the data which will be held inside of the store. That way we can get type checking and code completion support when writing our test cases.

Implementation of the select method is a bit different compared to the original NgRx Store. We’re not interested in testing NgRx so we basically accept any selector, ignore it and just return stored state as an Observable.

We are also introducing a new setState(data: T) method which does exactly what its name would suggest. The new state is then pushed to the the tested component which has previously subscribed to the TestStore using the select method.

The setState method is very useful for testing because it enables initialization of any possible state. That way we can focus on testing if UI displays this provided state correctly.

Both NgRx Store and our TestStore have also the dispatch method that accepts action objects. This is useful because we want to be able to test if component dispatched desired actions as a result of user UI interactions.

When to use TestStore?

TestStore is useful for unit testing of any component which injects NgRx Store in its constructor. These components have been traditionally called container or smart components.

On the other hand, TestStore should NOT be used when we want to unit test components which do not inject NgRx Store and receive all their data through @Input() bound attributes. These components have been traditionally known as view or dumb components.

Disclaimer — All code examples in this article are simplified to focus on what is the most important for any particular topic, please feel free to check todos.component.spec.ts file in the Angular NgRx Material Starter project to see the full example!

What we are going to test?

We’re going to use TestStore to write unit tests for the todos component. The component contains a list of todo items which can be filtered based on their done status.

UI of the todos component from Angular NgRx Material Starter, you can also try out live demo version

Simplified version of the component implementation can be summarized as injecting of the NgRx Store and subscribing for state updates to be able to display the todos list.

Using TestStore

Once we have TestStore available in our project we can use it to write component unit tests.

First we have to provide it in our testing module in place of original NgRx Store. We can do that by overriding Store provider with { provide: Store, useClass: TestStore }.

From now on, injecting Store will actually give us instance of the TestStore. Store can be in theory injected in every test individually but it is much more convenient to inject it only once in the beforeEach block and save it in a variable for further use.

TestStore support generics so we can specify type (or interface) of our TestStore (eg TestStore<TodosState>). This way we can be sure that we’re writing correct test code and that editor can help use with advanced code completion.
Example of how to provide our new TestStore instead of default NgRx Store in the test setup, feel free to check out final test implementation

In the previous code example we imported the TestStore from curiously looking "@testing/utils" module. Even though it looks like a scoped external npm dependency from the node_modules folder, it is in fact application code and the import is enabled by creating alias in the main tsconfig.json file.

Setting up aliases that enable us to import application code as if it came from node_modules instead of using long error prone relative paths (@testing/utils vs ../../../../testing/utils)

You can always import the TestStore using a relative import based on a particular location of your TestStore file and the test file (eg "../../testing/utils").

Follow me on Twitter to get notified about the newest blog posts and interesting frontend stuff

Testing of the component UI

Testing UI components is about making sure that they display correct UI elements in appropriate states based on the underlying data.

This seemingly simple idea can become quite hard to implement when we want to test components that are subscribed to state from NgRx Store. Initializing state then means that we have to emit appropriate and often quite specific actions before the actual testing.

We don’t want to know or care about implementation details of the reducers or actions just to initialize tests data

For these reasons TestStore comes with a store.setState() method that enables us to initialize any possible state whenever necessary. With the correct test state, all that is left to do is to check debug and native elements for the desired results…

Example of initialization of a test state using TestStore setState() method

Resulting tests are decoupled from the implementation details of how the specific state comes into existence.

In theory, this approach also enables us to swap our state management library without much effort as long as it has a store to which components subscribe for state updates.

Testing of Actions

Using NgRx to develop our applications means we often inject Store into our components so that they can subscribe to the updates of the relevant slice of the application state.

Another reason for injecting Store is to be able to dispatch NgRx actions. Actions are the only way to change (mutate) state in our Store.

From the testing perspective, we want to be able to verify that the component dispatched expected actions as a result of a specific UI interaction like clicking on a button.

In our example, the todos component contains a menu that enables us to select how we want to filter displayed todo items.

Clicking on any of the provided values will dispatch action which sets new filter value in the store.

The simplified implementation of such functionality can look something like this…

Again, we only want to test the component and NOT if NgRx handles actions correctly or if reducer produces a correct new state. This means we’re only interested in the fact that component dispatched correct action with correct payload as a result of user UI interaction.


Do you want to learn more about how to get the best developer experience with NgRx reducers, actions and Typescript?

Dispatched actions can be tested using provided Jasmine spyOn function which enables us to intercept every call to the dispatch function of our TestStore.

All we have to do is to create and save reference to the dispatch by calling spyOn(store, 'dispatch'). We proceed by a simulation of the tested UI interaction by triggering UI events like opening of a dropdown menu and clicking on a particular element of that menu.

Testing if component dispatched appropriate actions after a specific UI interaction

The only thing left to do is to check if the dispatchSpy was called correct amount of times with expected actions and with the desired payloads.

It is also possible to reset spy state with dispatchSpy.reset() in case of a more complicated test scenario with multiple actions and assertion blocks but let's try to keep them as simple as possible.

Notice that we create spy inside of a individual test case instead of the more common beforeEach block. The reason for this is to prevent recording of actions which might have been triggered and recorded as a result of other setup activities. This might be adjusted individually based on a particular component so feel free to create spy in beforeEach as long as you don't find yourself calling a lot of dispatchSpy.reset().

What did we learn?

We can simplify our tests and remove coupling between the modules by using NgRx TestStore. Using the store has following advantages:

  1. No dependency and hence no import of the reducers and corresponding actions from possibly various application modules
  2. No need to initialize tests state by dispatching real actions
  3. No need to emit actions also result in no triggered NgRx effects so we don't have to mock them to prevent undesirable behaviour

What we end up with are just plain old unit tests which are easy to set up, change and understand.

Feel free to check out Angular NgRx Material Starter project for more test examples.

Did you always wanted to contribute to open source? Get started by adding more tests following approaches described in this article. Check out list of open issues!

That’s it for today!

Please support this article with your 👏👏👏 to help it spread to a wider audience 🙏.

And never forget, future is bright
Obviously the bright future (📷 by NASA)
Like what you read? Give Tomas Trajan a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.