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.
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.
A couple of months after I’ve written this post, I had the pleasure to refactor some parts of the application I was working on. This application had a fairly amount of selector tests. During the refactor, a lot of the time was spent on keeping the selector tests green.
In these months I’ve learned some tips and tricks on how to write tests, but more important on what to test. A selector is an implementation detail and in most of the cases, you don’t what to test these. I’ve learned that if there are tests on the components, it’s often enough to cover mainly all of the selectors. This is because the component uses these selectors.
Doing this has the benefit that you won’t have to maintain these selector tests over time, and you still have the confidence that the component’s do their job.
I do still test the more complicated selectors, e.g. selectors with calculations inside of them. To write these tests I still use the methods mentioned below.
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:
- Every test creates its own isolated state, there is no global state
- It’s possible to override specific properties if needed, but a default state is provided
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.
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.
getCartItems snapshot looks like:
TIP: It could be helpful to add the state in the test description by using
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
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
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.
- A selector can be unit tested, you don’t need a (mocked) store in order to test your selectors.
- Use factory functions to setup state in order to prevent fragile tests.
- Each one of these approaches has its use case to test your selectors in a NgRx application.
Please consider supporting me if have you enjoyed this post and found it useful: