Tim Deschryver

How I test my NgRx selectors

The NgRx logoThe Angular logo
@tim_deschryver

See Testing an NgRx project for a complete guide on how to test a project that extensively uses NgRx.

In this post I’m going to show you how I test my selectors by putting the selectors from a previous post Clean NgRx reducers using Immer, where we created a small shopping cart application, under test. In the application there is a collection of products (the catalog) and the cart items, together they form the state of the application.

What exactly is a selector

A selector is a pure function that takes the state as an argument and returns a slice of the store state. You can see the selectors in an application as queries to retrieve slices of store state. Besides using selectors inside your components it is also possible to re-use selector functions inside other selectors.

A function is pure when: - Given the same input, it always return the same output
- Produces no side effects

Because selectors are pure functions, it can use an optimization technique called memoization. Meaning that the selector will store the outputs in a cache, if the selector gets called again with the same input it doesn’t have to re-execute the select function but it immediately can return the cached output.

These are the selectors which are used in the shopping cart application:

If you’re interested in the whole application you can take a look at the GitHub repository.

Knowing which selectors to test

I don't write tests to all of my selectors. Don't put time into testing selectors that simply pluck something from the state, it has almost no chance of being wrong. Use the gained time to better test the selectors that have logic in them, and thus where things can go wrong.

Setting up test state with factory functions

Before we’re going to create the tests, let’s create some factory functions to set up our state in each test.

I use (and like) this approach to prevent fragile tests, because of the following reasons:

Testing approach #1: “Default”

This is probably the most familiar way of testing selectors, it boils down to calling the selectors with the created state. The assertions are written based on the output of the selector to ensure it is returning the right data. There is actually nothing special to say here, so I’ll just show the test cases.

We can make use of a reference equality -toBe - because the selector simply returns a slice of the store state. These selectors are called getter selectors.

Testing Approach #2: Snapshots

I tend to find the above way repeatable some times, especially for the simpler selectors which don’t contain any logic. Because there is no logic, we can assume that the selector always returns the same output for the same type of input.

In my opinion these selectors are perfect for a snapshot test. A snapshot test creates a snapshot from the selector output the first time the test is run, the second time the same test is run it will compare the current output to the snapshot’s version. The test passes if the versions are identical, if these are not, you can either fix the selector or update the snapshot if needed.

For these tests I’m using a different way to create the tests. The first step is to create a testCases array. Each test case has a name (this is the name of the test), the selector function to put under test and the state which is the input of the selector. With the test cases in place, I’m going to loop over each one of them and I’m going to invoke the selector with the state, the output is used to create the snapshot.

NOTE: the tests below are written with Jest and not with Jasmine. In order to make use of snapshot testing with Jasmine, you’ll have to install a library.

The getCartItems snapshot looks like:

TIP: It could be helpful to add the state in the test description by using JSON.stringify.

Testing Approach #3: Projector

I use this approach when I’m dealing with selectors that need a lot of state setup or if the selector contains some logic.

A reason why you would need to setup a whole state tree, would be when you’re testing a selector that uses the output from several other selectors and derives some state from these outputs. These kind of selectors are named derive selectors.

Luckily NgRx provides a way to skip these large setups with a projector function. Every selector has a projector function that you can use in order to skip the execution from the other selectors and directly pass their outputs to the selector.

If we take a look at the getCartSummary selector, it uses the getAllCartSummary selector, which in turn uses the getProducts and getCartItems selectors. If we wouldn’t take advantage of the projector function, we would have to setup the whole state. In this little application it’s doable but in larger application this can be time consuming and even worse, it can be the cause of fragile tests, this is what I (and you should too) want to avoid.

As shown in the example below, I’m setting up the output that otherwise would be returned from getAllCartSummary, this output is passed to the getCartSummary selector by using the projector function.

This approach is also useful if you have some logic inside your selector. For example if you need to filter out specific items based on different properties, you can create a different state (which is easy because you’re using factory functions now) for each scenario.

Conclusion

Don't put time into testing selectors that simply pluck something from the state, it has almost no chance of being wrong. Use the gained time to better test the selectors that have logic in them, and thus where things can go wrong.

Not to miss

Come check out ngx-testing-library, an Angular testing library to test Angular components I wrote last week. The library is based on the dom-testing-library from Kent C. Dodds.

🚨 Introducing ngx-testing-library 🚨

Outgoing links
Support me

I appreciate it if you would support me if have you enjoyed this post and found it useful, thank you in advance.

Buy Me a Coffee at ko-fi.com PayPal logo
Support the blog Share on Twitter Discuss on Twitter Edit on GitHub